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

SentryTraceMiddleware   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Test Coverage

Coverage 66.34%

Importance

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

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getTransaction() 0 3 1
A process() 0 9 1
A setTransaction() 0 5 1
A startTransaction() 0 39 3
A updateTransactionNameIfDefault() 0 18 4
A terminate() 0 18 4
A getStartTime() 0 15 5
A hydrateRequestData() 0 20 3
A hydrateResponseData() 0 3 1
A addAppBootstrapSpan() 0 29 4
A addBootDetailTimeSpans() 0 15 4
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 1
            'url' => '/' . ltrim($request->getUri()->getPath(), '/'),
94 1
            'method' => strtoupper($request->getMethod()),
95
        ]);
96 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

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