Passed
Pull Request — master (#19)
by
unknown
02:53
created

SentryTraceMiddleware   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 236
Duplicated Lines 0 %

Test Coverage

Coverage 63.11%

Importance

Changes 0
Metric Value
eloc 101
dl 0
loc 236
ccs 65
cts 103
cp 0.6311
rs 9.76
c 0
b 0
f 0
wmc 33

13 Methods

Rating   Name   Duplication   Size   Complexity  
A startTransaction() 0 43 5
A __construct() 0 5 1
A updateTransactionNameIfDefault() 0 20 4
A getTransaction() 0 3 1
A setAppSpan() 0 3 1
A process() 0 9 1
A setTransaction() 0 5 1
A terminate() 0 18 4
A hydrateRequestData() 0 20 3
A hydrateResponseData() 0 3 1
A addAppBootstrapSpan() 0 33 6
A addBootDetailTimeSpans() 0 16 4
A getAppSpan() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Sentry\Tracing;
6
7
use Psr\Http\Message\ResponseInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Http\Server\MiddlewareInterface;
10
use Psr\Http\Server\RequestHandlerInterface;
11
use Sentry\SentrySdk;
12
use Sentry\State\HubInterface;
13
use Sentry\Tracing\Span;
14
use Sentry\Tracing\SpanContext;
15
use Sentry\Tracing\Transaction;
16
use Sentry\Tracing\TransactionContext;
17
use Yiisoft\Router\CurrentRoute;
18
use Yiisoft\Yii\Sentry\Integration\Integration;
19
20
final class SentryTraceMiddleware implements MiddlewareInterface
21
{
22
    /**
23
     * The current active transaction.
24
     *
25
     * @psalm-suppress PropertyNotSetInConstructor
26
     */
27
    protected ?\Sentry\Tracing\Transaction $transaction = null;
28
    /**
29
     * The span for the `app.handle` part of the application.
30
     *
31
     * @psalm-suppress PropertyNotSetInConstructor
32
     */
33
    protected ?\Sentry\Tracing\Span $appSpan = null;
34
    /**
35
     * The timestamp of application bootstrap completion.
36
     *
37
     * @var float|null
38
     */
39
    private ?float $bootedTimestamp;
40
    /**
41
     * @psalm-suppress PropertyNotSetInConstructor
42
     */
43
    private ?ServerRequestInterface $request = null;
44
    /**
45
     * @psalm-suppress PropertyNotSetInConstructor
46
     */
47
    private ?ResponseInterface $response = null;
48
49 1
    public function __construct(
50
        private HubInterface $hub,
51
        private ?CurrentRoute $currentRoute
52
    ) {
53 1
        $this->bootedTimestamp = microtime(true);
0 ignored issues
show
Documentation Bug introduced by
It seems like microtime(true) can also be of type string. However, the property $bootedTimestamp is declared as type double|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
54
    }
55
56
    public function getTransaction(): ?Transaction
57
    {
58
        return $this->transaction;
59
    }
60
61
    public function setTransaction(?Transaction $transaction): self
62
    {
63
        $this->transaction = $transaction;
64
65
        return $this;
66
    }
67
68 1
    public function process(
69
        ServerRequestInterface $request,
70
        RequestHandlerInterface $handler
71
    ): ResponseInterface {
72 1
        $this->startTransaction($request, $this->hub);
73 1
        $this->request = $request;
74 1
        $this->response = $handler->handle($request);
75
76 1
        return $this->response;
77
    }
78
79 1
    private function startTransaction(
80
        ServerRequestInterface $request,
81
        HubInterface $sentry
82
    ): void {
83 1
        $requestStartTime = !empty($request->getServerParams()['REQUEST_TIME_FLOAT'])
84
            ? (float)$request->getServerParams()['REQUEST_TIME_FLOAT']
85 1
            : (defined('APP_START_TIME')
86
                ? (float)APP_START_TIME
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Yii\Sentry\Tracing\APP_START_TIME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
87 1
                : microtime(true));
88
89 1
        if ($request->hasHeader('sentry-trace')) {
90
            $headers = $request->getHeader('sentry-trace');
91
            $header = reset($headers);
92
            $context = TransactionContext::fromSentryTrace($header);
93
        } else {
94 1
            $context = new TransactionContext();
95
        }
96
97 1
        $context->setOp('http.server');
98 1
        $context->setData([
99 1
            'url' => '/' . ltrim($request->getUri()->getPath(), '/'),
100 1
            'method' => strtoupper($request->getMethod()),
101
        ]);
102 1
        $context->setStartTimestamp($requestStartTime);
0 ignored issues
show
Bug introduced by
It seems like $requestStartTime can also be of type string; however, parameter $startTimestamp of Sentry\Tracing\SpanContext::setStartTimestamp() does only seem to accept double|null, 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

102
        $context->setStartTimestamp(/** @scrutinizer ignore-type */ $requestStartTime);
Loading history...
103
104 1
        $this->transaction = $sentry->startTransaction($context);
105
106
        // Setting the Transaction on the Hub
107 1
        SentrySdk::getCurrentHub()->setSpan($this->transaction);
108
109 1
        $bootstrapSpan = $this->addAppBootstrapSpan($request);
110
111 1
        $appContextStart = new SpanContext();
112 1
        $appContextStart->setOp('app.handle');
113 1
        $appContextStart->setStartTimestamp(
114 1
            $bootstrapSpan
115
                ? $bootstrapSpan->getEndTimestamp()
116 1
                : microtime(true)
117
        );
118
119 1
        $this->appSpan = $this->transaction->startChild($appContextStart);
120
121 1
        SentrySdk::getCurrentHub()->setSpan($this->appSpan);
122
    }
123
124 1
    private function addAppBootstrapSpan(ServerRequestInterface $request): ?Span
125
    {
126 1
        if ($this->bootedTimestamp === null) {
127
            return null;
128
        }
129 1
        if (null === $this->transaction) {
130
            return null;
131
        }
132
133 1
        $appStartTime = defined('APP_START_TIME')
134
            ? (float)APP_START_TIME
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Yii\Sentry\Tracing\APP_START_TIME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
135 1
            : (empty($request->getServerParams()['REQUEST_TIME_FLOAT'])
136 1
                ? null
137 1
                : (float)$request->getServerParams()['REQUEST_TIME_FLOAT']);
138
139 1
        if ($appStartTime === null) {
140 1
            return null;
141
        }
142
143
        $spanContextStart = new SpanContext();
144
        $spanContextStart->setOp('app.bootstrap');
145
        $spanContextStart->setStartTimestamp($appStartTime);
146
        $spanContextStart->setEndTimestamp($this->bootedTimestamp);
147
148
        $span = $this->transaction->startChild($spanContextStart);
149
150
        // Consume the booted timestamp, because we don't want to report the bootstrap span more than once
151
        $this->bootedTimestamp = null;
152
153
        // Add more information about the bootstrap section if possible
154
        $this->addBootDetailTimeSpans($span);
155
156
        return $span;
157
    }
158
159
    private function addBootDetailTimeSpans(Span $bootstrap): void
160
    {
161
        if (
162
            !defined('SENTRY_AUTOLOAD')
163
            || !SENTRY_AUTOLOAD
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Yii\Sentry\Tracing\SENTRY_AUTOLOAD was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
164
            || !is_numeric(SENTRY_AUTOLOAD)
165
        ) {
166
            return;
167
        }
168
169
        $autoload = new SpanContext();
170
        $autoload->setOp('autoload');
171
        $autoload->setStartTimestamp($bootstrap->getStartTimestamp());
172
        $autoload->setEndTimestamp((float)SENTRY_AUTOLOAD);
173
174
        $bootstrap->startChild($autoload);
175
    }
176
177 1
    public function terminate(): void
178
    {
179 1
        if ($this->transaction !== null) {
180 1
            $this->appSpan?->finish();
181
182
            // Make sure we set the transaction and not have a child span in the Sentry SDK
183
            // If the transaction is not on the scope during finish, the trace.context is wrong
184 1
            SentrySdk::getCurrentHub()->setSpan($this->transaction);
185
186 1
            if (null !== $this->request) {
187 1
                $this->hydrateRequestData($this->request);
188
            }
189
190 1
            if (null !== $this->response) {
191 1
                $this->hydrateResponseData($this->response);
192
            }
193
194 1
            $this->transaction->finish();
195
        }
196
    }
197
198 1
    private function hydrateRequestData(ServerRequestInterface $request): void
199
    {
200 1
        $route = $this->currentRoute;
201 1
        if (null === $this->transaction) {
202
            return;
203
        }
204
205 1
        if ($route) {
206 1
            $this->updateTransactionNameIfDefault(
207 1
                Integration::extractNameForRoute($route)
208
            );
209
210 1
            $this->transaction->setData([
211 1
                'name' => Integration::extractNameForRoute($route),
212 1
                'method' => $request->getMethod(),
213
            ]);
214
        }
215
216 1
        $this->updateTransactionNameIfDefault(
217 1
            '/' . ltrim($request->getUri()->getPath(), '/')
218
        );
219
    }
220
221 1
    private function updateTransactionNameIfDefault(?string $name): void
222
    {
223
        // Ignore empty names (and `null`) for caller convenience
224 1
        if (empty($name)) {
225 1
            return;
226
        }
227 1
        if (null === $this->transaction) {
228
            return;
229
        }
230
        // If the transaction already has a name other than the default
231
        // ignore the new name, this will most occur if the user has set a
232
        // transaction name themself before the application reaches this point
233
        if (
234 1
            $this->transaction->getName()
235
            !== TransactionContext::DEFAULT_NAME
236
        ) {
237
            return;
238
        }
239
240 1
        $this->transaction->setName($name);
241
    }
242
243 1
    private function hydrateResponseData(ResponseInterface $response): void
244
    {
245 1
        $this->transaction?->setHttpStatus($response->getStatusCode());
246
    }
247
248
    public function getAppSpan(): ?Span
249
    {
250
        return $this->appSpan;
251
    }
252
253
    public function setAppSpan(?Span $appSpan): void
254
    {
255
        $this->appSpan = $appSpan;
256
    }
257
}
258