PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.15
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.15
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
woocommerce-pos / vendor_prefixed / sentry / sentry / src / Client.php

Client.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.15, at vendor_prefixed/sentry/sentry/src/Client.php

419 lines 16.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare (strict_types=1);
4 namespace WCPOS\Vendor\Sentry;
5
6 use WCPOS\Vendor\Psr\Log\LoggerInterface;
7 use WCPOS\Vendor\Psr\Log\NullLogger;
8 use WCPOS\Vendor\Sentry\Integration\IntegrationInterface;
9 use WCPOS\Vendor\Sentry\Integration\IntegrationRegistry;
10 use WCPOS\Vendor\Sentry\Serializer\RepresentationSerializer;
11 use WCPOS\Vendor\Sentry\Serializer\RepresentationSerializerInterface;
12 use WCPOS\Vendor\Sentry\State\Scope;
13 use WCPOS\Vendor\Sentry\Transport\Result;
14 use WCPOS\Vendor\Sentry\Transport\TransportInterface;
15 /**
16 * Default implementation of the {@see ClientInterface} interface.
17 */
18 class Client implements ClientInterface
19 {
20 /**
21 * The version of the protocol to communicate with the Sentry server.
22 */
23 public const PROTOCOL_VERSION = '7';
24 /**
25 * The identifier of the SDK.
26 */
27 public const SDK_IDENTIFIER = 'sentry.php';
28 /**
29 * The version of the SDK.
30 */
31 public const SDK_VERSION = '4.31.0';
32 /**
33 * Regex pattern to detect if a string is a regex pattern (starts and ends with / optionally followed by flags).
34 * Supported flags: i (case-insensitive), m (multiline), s (dotall), u (unicode).
35 */
36 private const REGEX_PATTERN_DETECTION = '/^\\/.*\\/[imsu]*$/';
37 /**
38 * @var Options The client options
39 */
40 private $options;
41 /**
42 * @var TransportInterface The transport
43 */
44 private $transport;
45 /**
46 * @var LoggerInterface The PSR-3 logger
47 */
48 private $logger;
49 /**
50 * @var array<string, IntegrationInterface> The stack of integrations
51 *
52 * @phpstan-var array<class-string<IntegrationInterface>, IntegrationInterface>
53 */
54 private $integrations;
55 /**
56 * @var StacktraceBuilder
57 */
58 private $stacktraceBuilder;
59 /**
60 * @var string The Sentry SDK identifier
61 */
62 private $sdkIdentifier;
63 /**
64 * @var string The SDK version of the Client
65 */
66 private $sdkVersion;
67 /**
68 * Constructor.
69 *
70 * @param Options $options The client configuration
71 * @param TransportInterface $transport The transport
72 * @param string|null $sdkIdentifier The Sentry SDK identifier
73 * @param string|null $sdkVersion The Sentry SDK version
74 * @param RepresentationSerializerInterface|null $representationSerializer The serializer for function arguments
75 * @param LoggerInterface|null $logger The PSR-3 logger
76 */
77 public function __construct(Options $options, TransportInterface $transport, ?string $sdkIdentifier = null, ?string $sdkVersion = null, ?RepresentationSerializerInterface $representationSerializer = null, ?LoggerInterface $logger = null)
78 {
79 $this->options = $options;
80 $this->transport = $transport;
81 $this->sdkIdentifier = $sdkIdentifier ?? self::SDK_IDENTIFIER;
82 $this->sdkVersion = $sdkVersion ?? self::SDK_VERSION;
83 $this->stacktraceBuilder = new StacktraceBuilder($options, $representationSerializer ?? new RepresentationSerializer($this->options));
84 $this->logger = $logger ?? new NullLogger();
85 $this->integrations = IntegrationRegistry::getInstance()->setupIntegrations($options, $this->logger);
86 }
87 /**
88 * {@inheritdoc}
89 */
90 public function getOptions() : Options
91 {
92 return $this->options;
93 }
94 /**
95 * {@inheritdoc}
96 */
97 public function getCspReportUrl() : ?string
98 {
99 $dsn = $this->options->getDsn();
100 if ($dsn === null) {
101 return null;
102 }
103 $endpoint = $dsn->getCspReportEndpointUrl();
104 $query = \array_filter(['sentry_release' => $this->options->getRelease(), 'sentry_environment' => $this->options->getEnvironment()]);
105 if (!empty($query)) {
106 $endpoint .= '&' . \http_build_query($query, '', '&');
107 }
108 return $endpoint;
109 }
110 /**
111 * {@inheritdoc}
112 */
113 public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null) : ?EventId
114 {
115 $event = Event::createEvent();
116 $event->setMessage($message);
117 $event->setLevel($level);
118 return $this->captureEvent($event, $hint, $scope);
119 }
120 /**
121 * {@inheritdoc}
122 */
123 public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null) : ?EventId
124 {
125 $className = \get_class($exception);
126 if ($this->shouldIgnoreException($className)) {
127 $this->logger->info('The exception will be discarded because it matches an entry in "ignore_exceptions".', ['className' => $className]);
128 return null;
129 // short circuit to avoid unnecessary processing
130 }
131 $hint = $hint ?? new EventHint();
132 if ($hint->exception === null) {
133 $hint->exception = $exception;
134 }
135 return $this->captureEvent(Event::createEvent(), $hint, $scope);
136 }
137 /**
138 * {@inheritdoc}
139 */
140 public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null) : ?EventId
141 {
142 // Client reports don't need to be augmented in the prepareEvent pipeline.
143 if ($event->getType() !== EventType::clientReport()) {
144 $event = $this->prepareEvent($event, $hint, $scope);
145 }
146 if ($event === null) {
147 return null;
148 }
149 try {
150 /** @var Result $result */
151 $result = $this->transport->send($event);
152 $event = $result->getEvent();
153 if ($event !== null) {
154 return $event->getId();
155 }
156 } catch (\Throwable $exception) {
157 $this->logger->error(\sprintf('Failed to send the event to Sentry. Reason: "%s".', $exception->getMessage()), ['exception' => $exception, 'event' => $event]);
158 }
159 return null;
160 }
161 /**
162 * {@inheritdoc}
163 */
164 public function captureLastError(?Scope $scope = null, ?EventHint $hint = null) : ?EventId
165 {
166 $error = \error_get_last();
167 if ($error === null || !isset($error['message'][0])) {
168 return null;
169 }
170 $exception = new \ErrorException(@$error['message'], 0, @$error['type'], @$error['file'], @$error['line']);
171 return $this->captureException($exception, $scope, $hint);
172 }
173 /**
174 * {@inheritdoc}
175 *
176 * @phpstan-template T of IntegrationInterface
177 */
178 public function getIntegration(string $className) : ?IntegrationInterface
179 {
180 /** @phpstan-var T|null */
181 return $this->integrations[$className] ?? null;
182 }
183 /**
184 * {@inheritdoc}
185 */
186 public function flush(?int $timeout = null) : Result
187 {
188 return $this->transport->close($timeout);
189 }
190 /**
191 * {@inheritdoc}
192 */
193 public function getStacktraceBuilder() : StacktraceBuilder
194 {
195 return $this->stacktraceBuilder;
196 }
197 /**
198 * @internal
199 */
200 public function getLogger() : LoggerInterface
201 {
202 return $this->logger;
203 }
204 /**
205 * @internal
206 */
207 public function getTransport() : TransportInterface
208 {
209 return $this->transport;
210 }
211 public function getSdkIdentifier() : string
212 {
213 return $this->sdkIdentifier;
214 }
215 public function getSdkVersion() : string
216 {
217 return $this->sdkVersion;
218 }
219 /**
220 * Assembles an event and prepares it to be sent of to Sentry.
221 *
222 * @param Event $event The payload that will be converted to an Event
223 * @param EventHint|null $hint May contain additional information about the event
224 * @param Scope|null $scope Optional scope which enriches the Event
225 *
226 * @return Event|null The prepared event object or null if it must be discarded
227 */
228 private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null) : ?Event
229 {
230 if ($hint !== null) {
231 if ($hint->exception !== null && empty($event->getExceptions())) {
232 $this->addThrowableToEvent($event, $hint->exception, $hint);
233 }
234 if ($hint->stacktrace !== null && $event->getStacktrace() === null) {
235 $event->setStacktrace($hint->stacktrace);
236 }
237 }
238 $this->addMissingStacktraceToEvent($event);
239 $event->setSdkIdentifier($this->sdkIdentifier);
240 $event->setSdkVersion($this->sdkVersion);
241 $event->setTags(\array_merge($this->options->getTags(), $event->getTags()));
242 if ($event->getServerName() === null) {
243 $event->setServerName($this->options->getServerName());
244 }
245 if ($event->getRelease() === null) {
246 $event->setRelease($this->options->getRelease());
247 }
248 if ($event->getEnvironment() === null) {
249 $event->setEnvironment($this->options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT);
250 }
251 $eventDescription = \sprintf('%s%s [%s]', $event->getLevel() !== null ? $event->getLevel() . ' ' : '', (string) $event->getType(), (string) $event->getId());
252 $isEvent = EventType::event() === $event->getType();
253 $sampleRate = $this->options->getSampleRate();
254 // only sample with the `sample_rate` on errors/messages
255 if ($isEvent && $sampleRate < 1 && \mt_rand(1, 100) / 100.0 > $sampleRate) {
256 $this->logger->info(\sprintf('The %s will be discarded because it has been sampled.', $eventDescription), ['event' => $event]);
257 return null;
258 }
259 $event = $this->applyIgnoreOptions($event, $eventDescription);
260 if ($event === null) {
261 return null;
262 }
263 if ($scope !== null) {
264 $beforeEventProcessors = $event;
265 $event = $scope->applyToEvent($event, $hint, $this->options);
266 if ($event === null) {
267 $this->logger->info(\sprintf('The %s will be discarded because one of the event processors returned "null".', $eventDescription), ['event' => $beforeEventProcessors]);
268 return null;
269 }
270 }
271 $beforeSendCallback = $event;
272 $event = $this->applyBeforeSendCallback($event, $hint);
273 if ($event === null) {
274 $this->logger->info(\sprintf('The %s will be discarded because the "%s" callback returned "null".', $eventDescription, $this->getBeforeSendCallbackName($beforeSendCallback)), ['event' => $beforeSendCallback]);
275 }
276 return $event;
277 }
278 /**
279 * Checks if an exception should be ignored based on configured patterns.
280 * Supports both class hierarchy matching and regex patterns.
281 * Patterns starting and ending with '/' are treated as regex patterns.
282 */
283 private function shouldIgnoreException(string $className) : bool
284 {
285 foreach ($this->options->getIgnoreExceptions() as $pattern) {
286 // Check for regex pattern (starts with / and ends with / optionally followed by flags)
287 if (\preg_match(self::REGEX_PATTERN_DETECTION, $pattern)) {
288 try {
289 if (\preg_match($pattern, $className)) {
290 return \true;
291 }
292 } catch (\Throwable $e) {
293 // Invalid regex pattern, log and skip
294 $this->logger->warning(\sprintf('Invalid regex pattern in ignore_exceptions: "%s". Error: %s', $pattern, $e->getMessage()));
295 continue;
296 }
297 } else {
298 // Class hierarchy check
299 if (\is_a($className, $pattern, \true)) {
300 return \true;
301 }
302 }
303 }
304 return \false;
305 }
306 /**
307 * Checks if a transaction should be ignored based on configured patterns.
308 * Supports both exact string matching and regex patterns.
309 * Patterns starting and ending with '/' are treated as regex patterns.
310 */
311 private function shouldIgnoreTransaction(string $transactionName) : bool
312 {
313 foreach ($this->options->getIgnoreTransactions() as $pattern) {
314 // Check for regex pattern (starts with / and ends with / optionally followed by flags)
315 if (\preg_match(self::REGEX_PATTERN_DETECTION, $pattern)) {
316 try {
317 if (\preg_match($pattern, $transactionName)) {
318 return \true;
319 }
320 } catch (\Throwable $e) {
321 // Invalid regex pattern, log and skip
322 $this->logger->warning(\sprintf('Invalid regex pattern in ignore_transactions: "%s". Error: %s', $pattern, $e->getMessage()));
323 continue;
324 }
325 } else {
326 // Exact string match
327 if ($transactionName === $pattern) {
328 return \true;
329 }
330 }
331 }
332 return \false;
333 }
334 private function applyIgnoreOptions(Event $event, string $eventDescription) : ?Event
335 {
336 if ($event->getType() === EventType::event()) {
337 $exceptions = $event->getExceptions();
338 if (empty($exceptions)) {
339 return $event;
340 }
341 foreach ($exceptions as $exception) {
342 if ($this->shouldIgnoreException($exception->getType())) {
343 $this->logger->info(\sprintf('The %s will be discarded because it matches an entry in "ignore_exceptions".', $eventDescription), ['event' => $event]);
344 return null;
345 }
346 }
347 }
348 if ($event->getType() === EventType::transaction()) {
349 $transactionName = $event->getTransaction();
350 if ($transactionName === null) {
351 return $event;
352 }
353 if ($this->shouldIgnoreTransaction($transactionName)) {
354 $this->logger->info(\sprintf('The %s will be discarded because it matches a entry in "ignore_transactions".', $eventDescription), ['event' => $event]);
355 return null;
356 }
357 }
358 return $event;
359 }
360 private function applyBeforeSendCallback(Event $event, ?EventHint $hint) : ?Event
361 {
362 switch ($event->getType()) {
363 case EventType::event():
364 return $this->options->getBeforeSendCallback()($event, $hint);
365 case EventType::transaction():
366 return $this->options->getBeforeSendTransactionCallback()($event, $hint);
367 case EventType::checkIn():
368 return $this->options->getBeforeSendCheckInCallback()($event, $hint);
369 default:
370 return $event;
371 }
372 }
373 private function getBeforeSendCallbackName(Event $event) : string
374 {
375 switch ($event->getType()) {
376 case EventType::transaction():
377 return 'before_send_transaction';
378 case EventType::checkIn():
379 return 'before_send_check_in';
380 default:
381 return 'before_send';
382 }
383 }
384 /**
385 * Optionally adds a missing stacktrace to the Event if the client is configured to do so.
386 *
387 * @param Event $event The Event to add the missing stacktrace to
388 */
389 private function addMissingStacktraceToEvent(Event $event) : void
390 {
391 if (!$this->options->shouldAttachStacktrace()) {
392 return;
393 }
394 // We should not add a stacktrace when the event already has one or contains exceptions
395 if ($event->getStacktrace() !== null || !empty($event->getExceptions())) {
396 return;
397 }
398 $event->setStacktrace($this->stacktraceBuilder->buildFromBacktrace(\debug_backtrace(0), __FILE__, __LINE__ - 3));
399 }
400 /**
401 * Stores the given exception in the passed event.
402 *
403 * @param Event $event The event that will be enriched with the exception
404 * @param \Throwable $exception The exception that will be processed and added to the event
405 * @param EventHint $hint Contains additional information about the event
406 */
407 private function addThrowableToEvent(Event $event, \Throwable $exception, EventHint $hint) : void
408 {
409 if ($exception instanceof \ErrorException && $event->getLevel() === null) {
410 $event->setLevel(Severity::fromError($exception->getSeverity()));
411 }
412 $exceptions = [];
413 do {
414 $exceptions[] = new ExceptionDataBag($exception, $this->stacktraceBuilder->buildFromException($exception), $hint->mechanism ?? new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, \true, ['code' => $exception->getCode()]));
415 } while ($exception = $exception->getPrevious());
416 $event->setExceptions($exceptions);
417 }
418 }
419