Passed
Pull Request — master (#19)
by
unknown
12:21
created

SentryTraceMiddleware::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
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 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
            $baggageHeaders = $request->hasHeader('baggage') ? $request->getHeader('baggage') : [];
86
            $sentryTraceHeader = (string)reset($headers);
87
            $baggageHeader = (string)reset($baggageHeaders);
88
            $context = TransactionContext::fromHeaders($sentryTraceHeader, $baggageHeader);
89
        } else {
90 1
            $context = new TransactionContext();
91
        }
92
93 1
        $context->setOp('http.server');
94 1
        $context->setData([
95 1
            'url' => '/' . ltrim($request->getUri()->getPath(), '/'),
96 1
            'method' => strtoupper($request->getMethod()),
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 1
                'name' => Integration::extractNameForRoute($route),
203 1
                'method' => $request->getMethod(),
204
            ]);
205
        }
206
207 1
        $this->updateTransactionNameIfDefault(
208 1
            '/' . ltrim($request->getUri()->getPath(), '/')
209
        );
210
    }
211
212 1
    private function updateTransactionNameIfDefault(?string $name): void
213
    {
214
        // Ignore empty names (and `null`) for caller convenience
215 1
        if (empty($name)) {
216 1
            return;
217
        }
218 1
        if (null === $this->transaction) {
219
            return;
220
        }
221
        // If the transaction already has a name other than the default
222
        // ignore the new name, this will most occur if the user has set a
223
        // transaction name themself before the application reaches this point
224 1
        if ($this->transaction->getName() !== TransactionContext::DEFAULT_NAME
225
        ) {
226
            return;
227
        }
228
229 1
        $this->transaction->setName($name);
230
    }
231
232 1
    private function hydrateResponseData(ResponseInterface $response): void
233
    {
234 1
        $this->transaction?->setHttpStatus($response->getStatusCode());
235
    }
236
237 1
    private function getStartTime(ServerRequestInterface $request): ?float
238
    {
239
        /** @psalm-suppress MixedAssignment */
240 1
        $attStartTime = $request->getAttribute('applicationStartTime');
241 1
        if (is_numeric($attStartTime) && !empty((float)$attStartTime)) {
242
            $requestStartTime = (float)$attStartTime;
243
        } else {
244 1
            $requestStartTime = !empty($request->getServerParams()['REQUEST_TIME_FLOAT'])
245
                ? (float)$request->getServerParams()['REQUEST_TIME_FLOAT']
246 1
                : (defined('APP_START_TIME')
247
                    ? (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...
248 1
                    : null);
249
        }
250
251 1
        return $requestStartTime;
252
    }
253
}
254