BreadcrumbMiddleware::formatDuration()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
crap 3.0416
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Swis\Guzzle\Bugsnag;
6
7
use Bugsnag\Breadcrumbs\Breadcrumb;
8
use Bugsnag\BugsnagLaravel\Facades\Bugsnag;
9
use Bugsnag\Client;
10
use GuzzleHttp\BodySummarizerInterface;
11
use GuzzleHttp\Exception\GuzzleException;
12
use GuzzleHttp\Exception\RequestException;
13
use Psr\Http\Message\MessageInterface;
14
use Psr\Http\Message\RequestInterface;
15
use Psr\Http\Message\ResponseInterface;
16
use RuntimeException;
17
18
class BreadcrumbMiddleware
19
{
20
    protected Client $bugsnag;
21
22
    protected string $name;
23
24
    protected array $redactedStrings;
25
26
    protected ?BodySummarizerInterface $bodySummarizer;
27
28
    /**
29
     * @param \Bugsnag\Client $bugsnag         your (preconfigured) Bugsnag client
30
     * @param string          $name            the name of the breadcrumb
31
     * @param array           $redactedStrings a list of secret strings, such as API keys, that should be filtered out of the metadata
32
     * @param int|null        $truncateBodyAt  the length of the response body summary, which is added to the breadcrumb in case of client or server exceptions
33
     */
34 78
    public function __construct(
35
        Client $bugsnag,
36
        string $name = 'HTTP request',
37
        array $redactedStrings = [],
38
        ?int $truncateBodyAt = 512
39
    ) {
40 78
        $this->bugsnag = $bugsnag;
41 78
        $this->name = $name;
42 78
        $this->redactedStrings = $redactedStrings;
43 78
        $this->bodySummarizer = $truncateBodyAt ? new BodySummarizer($truncateBodyAt) : null;
44 39
    }
45
46
    /**
47
     * Create a new instance using the Bugsnag facade.
48
     *
49
     * @param mixed ...$arguments
50
     *
51
     * @return self
52
     */
53 6
    public static function fromFacade(...$arguments): self
54
    {
55 6
        if (!class_exists(Bugsnag::class)) {
56
            throw new RuntimeException('Creating the middleware from the Bugsnag facade requires Laravel and the bugsnag/bugsnag-laravel package.');
57
        }
58
59 6
        return new self(Bugsnag::getFacadeRoot(), ...$arguments);
0 ignored issues
show
Bug introduced by
$arguments is expanded, but the parameter $name of Swis\Guzzle\Bugsnag\Brea...ddleware::__construct() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

59
        return new self(Bugsnag::getFacadeRoot(), /** @scrutinizer ignore-type */ ...$arguments);
Loading history...
Bug introduced by
It seems like Bugsnag\BugsnagLaravel\F...ugsnag::getFacadeRoot() can also be of type null; however, parameter $bugsnag of Swis\Guzzle\Bugsnag\Brea...ddleware::__construct() does only seem to accept Bugsnag\Client, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

59
        return new self(/** @scrutinizer ignore-type */ Bugsnag::getFacadeRoot(), ...$arguments);
Loading history...
60
    }
61
62
    /**
63
     * @param \GuzzleHttp\BodySummarizerInterface|null $bodySummarizer
64
     *
65
     * @return $this
66
     */
67 18
    public function setBodySummarizer(?BodySummarizerInterface $bodySummarizer): self
68
    {
69 18
        $this->bodySummarizer = $bodySummarizer;
70
71 18
        return $this;
72
    }
73
74
    /**
75
     * @param callable $handler
76
     *
77
     * @return callable
78
     */
79 72
    public function __invoke(callable $handler): callable
80
    {
81 72
        return function (RequestInterface $request, array $options) use ($handler) {
82
            // Set starting time.
83 72
            $start = microtime(true);
84
85 72
            return $handler($request, $options)
86 72
                ->then(function (ResponseInterface $response) use ($start, $request) {
87
                    // After
88 66
                    $this->leaveBreadcrumb($start, $request, $response);
89
90 66
                    return $response;
91 72
                }, function (GuzzleException $exception) use ($start, $request) {
92 6
                    $response = $exception instanceof RequestException ? $exception->getResponse() : null;
93
94 6
                    $this->leaveBreadcrumb($start, $request, $response);
95
96 6
                    throw $exception;
97 36
                });
98 36
        };
99
    }
100
101 72
    protected function leaveBreadcrumb($start, RequestInterface $request, ResponseInterface $response = null): void
102
    {
103 72
        $this->bugsnag->leaveBreadcrumb(
104 72
            $this->name,
105 36
            Breadcrumb::PROCESS_TYPE,
106 72
            array_filter([
107 72
                'method' => $request->getMethod(),
108 72
                'uri' => $this->filter((string) $request->getUri()),
109 72
                'requestBody' => $this->filter($this->summarize($request)),
110 72
                'statusCode' => $response ? $response->getStatusCode() : null,
111 72
                'responseBody' => $response ? $this->filter($this->summarize($response)) : null,
112 72
                'time' => $this->formatDuration(microtime(true) - $start),
113
            ])
114
        );
115 36
    }
116
117 72
    protected function filter(?string $string): ?string
118
    {
119 72
        return $string ? str_replace($this->redactedStrings, '[FILTERED]', $string) : null;
120
    }
121
122 72
    protected function summarize(MessageInterface $request): ?string
123
    {
124 72
        return $this->bodySummarizer ? $this->bodySummarizer->summarize($request) : null;
125
    }
126
127
    /**
128
     * @param float $seconds
129
     *
130
     * @return string
131
     */
132 72
    protected function formatDuration(float $seconds): string
133
    {
134 72
        if ($seconds < 0.001) {
135 68
            return round($seconds * 1000000).'μs';
136
        }
137
138 4
        if ($seconds < 1) {
139 4
            return round($seconds * 1000, 2).'ms';
140
        }
141
142
        return round($seconds, 2).'s';
143
    }
144
}
145