Test Failed
Pull Request — master (#19)
by
unknown
04:32 queued 01:40
created

SentryTraceMiddleware::getStartTime()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
nc 5
nop 1
dl 0
loc 14
rs 9.6111
c 1
b 0
f 0
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
    private ?float $bootedTimestamp;
38
    /**
39
     * @psalm-suppress PropertyNotSetInConstructor
40
     */
41
    private ?ServerRequestInterface $request = null;
42
    /**
43
     * @psalm-suppress PropertyNotSetInConstructor
44
     */
45
    private ?ResponseInterface $response = null;
46
47
    public function __construct(
48
        private HubInterface $hub,
49
        private ?CurrentRoute $currentRoute
50
    ) {
51
        $this->bootedTimestamp = microtime(true);
52
    }
53
54
    public function getTransaction(): ?Transaction
55
    {
56
        return $this->transaction;
57
    }
58
59
    public function setTransaction(?Transaction $transaction): self
60
    {
61
        $this->transaction = $transaction;
62
63
        return $this;
64
    }
65
66
    public function process(
67
        ServerRequestInterface $request,
68
        RequestHandlerInterface $handler
69
    ): ResponseInterface {
70
        $this->startTransaction($request, $this->hub);
71
        $this->request = $request;
72
        $this->response = $handler->handle($request);
73
74
        return $this->response;
75
    }
76
77
    private function startTransaction(
78
        ServerRequestInterface $request,
79
        HubInterface $sentry
80
    ): void {
81
        $requestStartTime = $this->getStartTime($request) ?? microtime(true);
82
83
        if ($request->hasHeader('sentry-trace')) {
84
            $headers = $request->getHeader('sentry-trace');
85
            $header = reset($headers);
86
            $context = TransactionContext::fromSentryTrace($header);
87
        } else {
88
            $context = new TransactionContext();
89
        }
90
91
        $context->setOp('http.server');
92
        $context->setData(
93
            [
94
            'url' => '/' . ltrim($request->getUri()->getPath(), '/'),
95
            'method' => strtoupper($request->getMethod()),
96
            ]
97
        );
98
        $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

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