vendor/sentry/sentry/src/Client.php line 88

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sentry;
  4. use GuzzleHttp\Promise\PromiseInterface;
  5. use Psr\Log\LoggerInterface;
  6. use Psr\Log\NullLogger;
  7. use Sentry\Integration\IntegrationInterface;
  8. use Sentry\Integration\IntegrationRegistry;
  9. use Sentry\Serializer\RepresentationSerializer;
  10. use Sentry\Serializer\RepresentationSerializerInterface;
  11. use Sentry\Serializer\SerializerInterface;
  12. use Sentry\State\Scope;
  13. use Sentry\Transport\TransportInterface;
  14. /**
  15. * Default implementation of the {@see ClientInterface} interface.
  16. *
  17. * @author Stefano Arlandini <sarlandini@alice.it>
  18. */
  19. final class Client implements ClientInterface
  20. {
  21. /**
  22. * The version of the protocol to communicate with the Sentry server.
  23. */
  24. public const PROTOCOL_VERSION = '7';
  25. /**
  26. * The identifier of the SDK.
  27. */
  28. public const SDK_IDENTIFIER = 'sentry.php';
  29. /**
  30. * The version of the SDK.
  31. */
  32. public const SDK_VERSION = '3.22.1';
  33. /**
  34. * @var Options The client options
  35. */
  36. private $options;
  37. /**
  38. * @var TransportInterface The transport
  39. */
  40. private $transport;
  41. /**
  42. * @var LoggerInterface The PSR-3 logger
  43. */
  44. private $logger;
  45. /**
  46. * @var array<string, IntegrationInterface> The stack of integrations
  47. *
  48. * @psalm-var array<class-string<IntegrationInterface>, IntegrationInterface>
  49. */
  50. private $integrations;
  51. /**
  52. * @var StacktraceBuilder
  53. */
  54. private $stacktraceBuilder;
  55. /**
  56. * @var string The Sentry SDK identifier
  57. */
  58. private $sdkIdentifier;
  59. /**
  60. * @var string The SDK version of the Client
  61. */
  62. private $sdkVersion;
  63. /**
  64. * Constructor.
  65. *
  66. * @param Options $options The client configuration
  67. * @param TransportInterface $transport The transport
  68. * @param string|null $sdkIdentifier The Sentry SDK identifier
  69. * @param string|null $sdkVersion The Sentry SDK version
  70. * @param SerializerInterface|null $serializer The serializer argument is deprecated since version 3.3 and will be removed in 4.0. It's currently unused.
  71. * @param RepresentationSerializerInterface|null $representationSerializer The serializer for function arguments
  72. * @param LoggerInterface|null $logger The PSR-3 logger
  73. */
  74. public function __construct(
  75. Options $options,
  76. TransportInterface $transport,
  77. ?string $sdkIdentifier = null,
  78. ?string $sdkVersion = null,
  79. ?SerializerInterface $serializer = null,
  80. ?RepresentationSerializerInterface $representationSerializer = null,
  81. ?LoggerInterface $logger = null
  82. ) {
  83. $this->options = $options;
  84. $this->transport = $transport;
  85. $this->logger = $logger ?? new NullLogger();
  86. $this->integrations = IntegrationRegistry::getInstance()->setupIntegrations($options, $this->logger);
  87. $this->stacktraceBuilder = new StacktraceBuilder($options, $representationSerializer ?? new RepresentationSerializer($this->options));
  88. $this->sdkIdentifier = $sdkIdentifier ?? self::SDK_IDENTIFIER;
  89. $this->sdkVersion = $sdkVersion ?? self::SDK_VERSION;
  90. }
  91. /**
  92. * {@inheritdoc}
  93. */
  94. public function getOptions(): Options
  95. {
  96. return $this->options;
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. public function getCspReportUrl(): ?string
  102. {
  103. $dsn = $this->options->getDsn();
  104. if (null === $dsn) {
  105. return null;
  106. }
  107. $endpoint = $dsn->getCspReportEndpointUrl();
  108. $query = array_filter([
  109. 'sentry_release' => $this->options->getRelease(),
  110. 'sentry_environment' => $this->options->getEnvironment(),
  111. ]);
  112. if (!empty($query)) {
  113. $endpoint .= '&' . http_build_query($query, '', '&');
  114. }
  115. return $endpoint;
  116. }
  117. /**
  118. * {@inheritdoc}
  119. */
  120. public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null): ?EventId
  121. {
  122. $event = Event::createEvent();
  123. $event->setMessage($message);
  124. $event->setLevel($level);
  125. return $this->captureEvent($event, $hint, $scope);
  126. }
  127. /**
  128. * {@inheritdoc}
  129. */
  130. public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null): ?EventId
  131. {
  132. $hint = $hint ?? new EventHint();
  133. if (null === $hint->exception) {
  134. $hint->exception = $exception;
  135. }
  136. return $this->captureEvent(Event::createEvent(), $hint, $scope);
  137. }
  138. /**
  139. * {@inheritdoc}
  140. */
  141. public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId
  142. {
  143. $event = $this->prepareEvent($event, $hint, $scope);
  144. if (null === $event) {
  145. return null;
  146. }
  147. try {
  148. /** @var Response $response */
  149. $response = $this->transport->send($event)->wait();
  150. $event = $response->getEvent();
  151. if (null !== $event) {
  152. return $event->getId();
  153. }
  154. } catch (\Throwable $exception) {
  155. }
  156. return null;
  157. }
  158. /**
  159. * {@inheritdoc}
  160. */
  161. public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): ?EventId
  162. {
  163. $error = error_get_last();
  164. if (null === $error || !isset($error['message'][0])) {
  165. return null;
  166. }
  167. $exception = new \ErrorException(@$error['message'], 0, @$error['type'], @$error['file'], @$error['line']);
  168. return $this->captureException($exception, $scope, $hint);
  169. }
  170. /**
  171. * {@inheritdoc}
  172. *
  173. * @psalm-template T of IntegrationInterface
  174. */
  175. public function getIntegration(string $className): ?IntegrationInterface
  176. {
  177. /** @psalm-var T|null */
  178. return $this->integrations[$className] ?? null;
  179. }
  180. /**
  181. * {@inheritdoc}
  182. */
  183. public function flush(?int $timeout = null): PromiseInterface
  184. {
  185. return $this->transport->close($timeout);
  186. }
  187. /**
  188. * {@inheritdoc}
  189. */
  190. public function getStacktraceBuilder(): StacktraceBuilder
  191. {
  192. return $this->stacktraceBuilder;
  193. }
  194. /**
  195. * Assembles an event and prepares it to be sent of to Sentry.
  196. *
  197. * @param Event $event The payload that will be converted to an Event
  198. * @param EventHint|null $hint May contain additional information about the event
  199. * @param Scope|null $scope Optional scope which enriches the Event
  200. *
  201. * @return Event|null The prepared event object or null if it must be discarded
  202. */
  203. private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?Event
  204. {
  205. if (null !== $hint) {
  206. if (null !== $hint->exception && empty($event->getExceptions())) {
  207. $this->addThrowableToEvent($event, $hint->exception, $hint);
  208. }
  209. if (null !== $hint->stacktrace && null === $event->getStacktrace()) {
  210. $event->setStacktrace($hint->stacktrace);
  211. }
  212. }
  213. $this->addMissingStacktraceToEvent($event);
  214. $event->setSdkIdentifier($this->sdkIdentifier);
  215. $event->setSdkVersion($this->sdkVersion);
  216. $event->setTags(array_merge($this->options->getTags(), $event->getTags()));
  217. if (null === $event->getServerName()) {
  218. $event->setServerName($this->options->getServerName());
  219. }
  220. if (null === $event->getRelease()) {
  221. $event->setRelease($this->options->getRelease());
  222. }
  223. if (null === $event->getEnvironment()) {
  224. $event->setEnvironment($this->options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT);
  225. }
  226. if (null === $event->getLogger()) {
  227. $event->setLogger($this->options->getLogger(false));
  228. }
  229. $isTransaction = EventType::transaction() === $event->getType();
  230. $sampleRate = $this->options->getSampleRate();
  231. if (!$isTransaction && $sampleRate < 1 && mt_rand(1, 100) / 100.0 > $sampleRate) {
  232. $this->logger->info('The event will be discarded because it has been sampled.', ['event' => $event]);
  233. return null;
  234. }
  235. $event = $this->applyIgnoreOptions($event);
  236. if (null === $event) {
  237. return null;
  238. }
  239. if (null !== $scope) {
  240. $beforeEventProcessors = $event;
  241. $event = $scope->applyToEvent($event, $hint, $this->options);
  242. if (null === $event) {
  243. $this->logger->info(
  244. 'The event will be discarded because one of the event processors returned "null".',
  245. ['event' => $beforeEventProcessors]
  246. );
  247. return null;
  248. }
  249. }
  250. $beforeSendCallback = $event;
  251. $event = $this->applyBeforeSendCallback($event, $hint);
  252. if (null === $event) {
  253. $this->logger->info(
  254. sprintf(
  255. 'The event will be discarded because the "%s" callback returned "null".',
  256. $this->getBeforeSendCallbackName($beforeSendCallback)
  257. ),
  258. ['event' => $beforeSendCallback]
  259. );
  260. }
  261. return $event;
  262. }
  263. private function applyIgnoreOptions(Event $event): ?Event
  264. {
  265. if ($event->getType() === EventType::event()) {
  266. $exceptions = $event->getExceptions();
  267. if (empty($exceptions)) {
  268. return $event;
  269. }
  270. foreach ($exceptions as $exception) {
  271. if (\in_array($exception->getType(), $this->options->getIgnoreExceptions(), true)) {
  272. $this->logger->info(
  273. 'The event will be discarded because it matches an entry in "ignore_exceptions".',
  274. ['event' => $event]
  275. );
  276. return null;
  277. }
  278. }
  279. }
  280. if ($event->getType() === EventType::transaction()) {
  281. $transactionName = $event->getTransaction();
  282. if (null === $transactionName) {
  283. return $event;
  284. }
  285. if (\in_array($transactionName, $this->options->getIgnoreTransactions(), true)) {
  286. $this->logger->info(
  287. 'The event will be discarded because it matches a entry in "ignore_transactions".',
  288. ['event' => $event]
  289. );
  290. return null;
  291. }
  292. }
  293. return $event;
  294. }
  295. private function applyBeforeSendCallback(Event $event, ?EventHint $hint): ?Event
  296. {
  297. if ($event->getType() === EventType::event()) {
  298. return ($this->options->getBeforeSendCallback())($event, $hint);
  299. }
  300. if ($event->getType() === EventType::transaction()) {
  301. return ($this->options->getBeforeSendTransactionCallback())($event, $hint);
  302. }
  303. return $event;
  304. }
  305. private function getBeforeSendCallbackName(Event $event): string
  306. {
  307. $beforeSendCallbackName = 'before_send';
  308. if ($event->getType() === EventType::transaction()) {
  309. $beforeSendCallbackName = 'before_send_transaction';
  310. }
  311. return $beforeSendCallbackName;
  312. }
  313. /**
  314. * Optionally adds a missing stacktrace to the Event if the client is configured to do so.
  315. *
  316. * @param Event $event The Event to add the missing stacktrace to
  317. */
  318. private function addMissingStacktraceToEvent(Event $event): void
  319. {
  320. if (!$this->options->shouldAttachStacktrace()) {
  321. return;
  322. }
  323. // We should not add a stacktrace when the event already has one or contains exceptions
  324. if (null !== $event->getStacktrace() || !empty($event->getExceptions())) {
  325. return;
  326. }
  327. $event->setStacktrace($this->stacktraceBuilder->buildFromBacktrace(
  328. debug_backtrace(0),
  329. __FILE__,
  330. __LINE__ - 3
  331. ));
  332. }
  333. /**
  334. * Stores the given exception in the passed event.
  335. *
  336. * @param Event $event The event that will be enriched with the exception
  337. * @param \Throwable $exception The exception that will be processed and added to the event
  338. * @param EventHint $hint Contains additional information about the event
  339. */
  340. private function addThrowableToEvent(Event $event, \Throwable $exception, EventHint $hint): void
  341. {
  342. if ($exception instanceof \ErrorException && null === $event->getLevel()) {
  343. $event->setLevel(Severity::fromError($exception->getSeverity()));
  344. }
  345. $exceptions = [];
  346. do {
  347. $exceptions[] = new ExceptionDataBag(
  348. $exception,
  349. $this->stacktraceBuilder->buildFromException($exception),
  350. $hint->mechanism ?? new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true, ['code' => $exception->getCode()])
  351. );
  352. } while ($exception = $exception->getPrevious());
  353. $event->setExceptions($exceptions);
  354. }
  355. }