Passed
Pull Request — master (#19)
by
unknown
04:36 queued 02:12
created

SentryTraceMiddleware::addAppBootstrapSpan()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 10
nop 1
dl 0
loc 33
ccs 0
cts 20
cp 0
crap 42
rs 9.0111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Sentry\Tracing;
6
7
use Yiisoft\Yii\Sentry\Integration;
8
use Psr\Http\Message\ResponseInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use Psr\Http\Server\MiddlewareInterface;
11
use Psr\Http\Server\RequestHandlerInterface;
12
use Sentry\SentrySdk;
13
use Sentry\State\HubInterface;
14
use Sentry\Tracing\Span;
15
use Sentry\Tracing\SpanContext;
16
use Sentry\Tracing\Transaction;
17
use Sentry\Tracing\TransactionContext;
18
use Yiisoft\Router\CurrentRoute;
19
20
final class SentryTraceMiddleware implements MiddlewareInterface
21
{
22
    /**
23
     * The current active transaction.
24
     *
25
     * @psalm-suppress PropertyNotSetInConstructor
26
     *
27
     * @var Transaction|null
28
     */
29
    protected $transaction;
30
    /**
31
     * The span for the `app.handle` part of the application.
32
     *
33
     * @psalm-suppress PropertyNotSetInConstructor
34
     *
35
     * @var Span|null
36
     */
37
    protected $appSpan;
38
    /**
39
     * The timestamp of application bootstrap completion.
40
     *
41
     * @var float|null
42
     */
43
    private ?float $bootedTimestamp;
44
    /**
45
     * @psalm-suppress PropertyNotSetInConstructor
46
     */
47
    private ?ServerRequestInterface $request;
48
    /**
49
     * @psalm-suppress PropertyNotSetInConstructor
50
     */
51
    private ?ResponseInterface $response;
52
53
    public function __construct(
54
        private HubInterface $hub,
55
        private ?CurrentRoute $currentRoute
56
    ) {
57
        $this->request = null;
58
        $this->response = null;
59
        $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...
60
    }
61
62
    /**
63
     * @return Transaction|null
64
     */
65
    public function getTransaction(): ?Transaction
66
    {
67
        return $this->transaction;
68
    }
69
70
    /**
71
     * @param Transaction|null $transaction
72
     *
73
     * @return self
74
     */
75
    public function setTransaction(?Transaction $transaction): self
76
    {
77
        $this->transaction = $transaction;
78
79
        return $this;
80
    }
81
82
    public function process(
83
        ServerRequestInterface $request,
84
        RequestHandlerInterface $handler
85
    ): ResponseInterface {
86
        $this->startTransaction($request, $this->hub);
87
        $this->request = $request;
88
        $this->response = $handler->handle($request);
89
90
        return $this->response;
91
    }
92
93
    private function startTransaction(
94
        ServerRequestInterface $request,
95
        HubInterface $sentry
96
    ): void {
97
        $requestStartTime = !empty($request->getServerParams()['REQUEST_TIME_FLOAT'])
98
            ? (float)$request->getServerParams()['REQUEST_TIME_FLOAT']
99
            : (defined('APP_START_TIME')
100
                ? (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...
101
                : microtime(true));
102
103
        if ($request->hasHeader('sentry-trace')) {
104
            $headers = $request->getHeader('sentry-trace');
105
            $header = reset($headers);
106
            $context = TransactionContext::fromSentryTrace($header);
107
        } else {
108
            $context = new TransactionContext();
109
        }
110
111
        $context->setOp('http.server');
112
        $context->setData([
113
            'url' => '/' . ltrim($request->getUri()->getPath(), '/'),
114
            'method' => strtoupper($request->getMethod()),
115
        ]);
116
        $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

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