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

SentryTraceMiddleware   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Test Coverage

Coverage 66.34%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 98
c 2
b 1
f 0
dl 0
loc 234
ccs 67
cts 101
cp 0.6634
rs 9.84
wmc 32

12 Methods

Rating   Name   Duplication   Size   Complexity  
A startTransaction() 0 41 3
A __construct() 0 5 1
A updateTransactionNameIfDefault() 0 18 4
A getTransaction() 0 3 1
A process() 0 9 1
A setTransaction() 0 5 1
A terminate() 0 18 4
A hydrateRequestData() 0 22 3
A hydrateResponseData() 0 3 1
A addAppBootstrapSpan() 0 29 4
A addBootDetailTimeSpans() 0 15 4
A getStartTime() 0 15 5
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 1
    public function __construct(
48
        private HubInterface $hub,
49
        private ?CurrentRoute $currentRoute
50
    ) {
51 1
        $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 1
    public function process(
67
        ServerRequestInterface $request,
68
        RequestHandlerInterface $handler
69
    ): ResponseInterface {
70 1
        $this->startTransaction($request, $this->hub);
71 1
        $this->request = $request;
72 1
        $this->response = $handler->handle($request);
73
74 1
        return $this->response;
75
    }
76
77 1
    private function startTransaction(
78
        ServerRequestInterface $request,
79
        HubInterface $sentry
80
    ): void {
81 1
        $requestStartTime = $this->getStartTime($request) ?? microtime(true);
82
83 1
        if ($request->hasHeader('sentry-trace')) {
84
            $headers = $request->getHeader('sentry-trace');
85
            $header = reset($headers);
86
            $context = TransactionContext::fromSentryTrace($header);
87
        } else {
88 1
            $context = new TransactionContext();
89
        }
90
91 1
        $context->setOp('http.server');
92 1
        $context->setData(
93
            [
94 1
            'url' => '/' . ltrim($request->getUri()->getPath(), '/'),
95 1
            'method' => strtoupper($request->getMethod()),
96
            ]
97
        );
98 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

98
        $context->setStartTimestamp(/** @scrutinizer ignore-type */ $requestStartTime);
Loading history...
99
100 1
        $this->transaction = $sentry->startTransaction($context);
101
102
        // Setting the Transaction on the Hub
103 1
        SentrySdk::getCurrentHub()->setSpan($this->transaction);
104
105 1
        $bootstrapSpan = $this->addAppBootstrapSpan($request);
106
107 1
        $appContextStart = new SpanContext();
108 1
        $appContextStart->setOp('app.handle');
109 1
        $appContextStart->setStartTimestamp(
110 1
            $bootstrapSpan
111
                ? $bootstrapSpan->getEndTimestamp()
112 1
                : microtime(true)
113
        );
114
115 1
        $this->appSpan = $this->transaction->startChild($appContextStart);
116
117 1
        SentrySdk::getCurrentHub()->setSpan($this->appSpan);
118
    }
119
120 1
    private function addAppBootstrapSpan(ServerRequestInterface $request): ?Span
121
    {
122 1
        if ($this->bootedTimestamp === null) {
123
            return null;
124
        }
125 1
        if (null === $this->transaction) {
126
            return null;
127
        }
128
129 1
        $appStartTime = $this->getStartTime($request);
130
131 1
        if ($appStartTime === null) {
132 1
            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 1
    public function terminate(): void
169
    {
170 1
        if ($this->transaction !== null) {
171 1
            $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 1
            SentrySdk::getCurrentHub()->setSpan($this->transaction);
176
177 1
            if (null !== $this->request) {
178 1
                $this->hydrateRequestData($this->request);
179
            }
180
181 1
            if (null !== $this->response) {
182 1
                $this->hydrateResponseData($this->response);
183
            }
184
185 1
            $this->transaction->finish();
186
        }
187
    }
188
189 1
    private function hydrateRequestData(ServerRequestInterface $request): void
190
    {
191 1
        $route = $this->currentRoute;
192 1
        if (null === $this->transaction) {
193
            return;
194
        }
195
196 1
        if ($route) {
197 1
            $this->updateTransactionNameIfDefault(
198 1
                Integration::extractNameForRoute($route)
199
            );
200
201 1
            $this->transaction->setData(
202
                [
203 1
                'name' => Integration::extractNameForRoute($route),
204 1
                'method' => $request->getMethod(),
205
                ]
206
            );
207
        }
208
209 1
        $this->updateTransactionNameIfDefault(
210 1
            '/' . ltrim($request->getUri()->getPath(), '/')
211
        );
212
    }
213
214 1
    private function updateTransactionNameIfDefault(?string $name): void
215
    {
216
        // Ignore empty names (and `null`) for caller convenience
217 1
        if (empty($name)) {
218 1
            return;
219
        }
220 1
        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 1
        if ($this->transaction->getName() !== TransactionContext::DEFAULT_NAME
227
        ) {
228
            return;
229
        }
230
231 1
        $this->transaction->setName($name);
232
    }
233
234 1
    private function hydrateResponseData(ResponseInterface $response): void
235
    {
236 1
        $this->transaction?->setHttpStatus($response->getStatusCode());
237
    }
238
239 1
    private function getStartTime(ServerRequestInterface $request): ?float
240
    {
241
        /** @psalm-suppress MixedAssignment */
242 1
        $attStartTime = $request->getAttribute('applicationStartTime');
243 1
        if (is_numeric($attStartTime) && !empty((float)$attStartTime)) {
244
            $requestStartTime = (float)$attStartTime;
245
        } else {
246 1
            $requestStartTime = !empty($request->getServerParams()['REQUEST_TIME_FLOAT'])
247
                ? (float)$request->getServerParams()['REQUEST_TIME_FLOAT']
248 1
                : (defined('APP_START_TIME')
249
                    ? (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...
250 1
                    : null);
251
        }
252
253 1
        return $requestStartTime;
254
    }
255
}
256