Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [ '8.1', '8.2', '8.3' ]
php: [ '8.1', '8.2', '8.3', '8.4' ]
typo3: [ '11', '12' ]
steps:
- name: Setup PHP with PECL extension
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- uses: actions/checkout@v2
- uses: actions/cache@v2
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.composer/cache/files
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
Expand Down
37 changes: 32 additions & 5 deletions Classes/Command/FlushCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
use Exception;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\RequestException;
use Http\Client\HttpAsyncClient;
use Jean85\Exception\VersionMissingExceptionInterface;
use Http\Client\Common\Exception\ClientErrorException;
use Http\Client\HttpAsyncClient;
use Http\Discovery\Psr17FactoryDiscovery;
use Jean85\Exception\VersionMissingExceptionInterface;
use Jean85\PrettyVersions;
use Pluswerk\Sentry\Queue\Entry;
use Pluswerk\Sentry\Queue\QueueInterface;
use Pluswerk\Sentry\Service\Sentry;
use Psr\Http\Message\ResponseInterface;
use Sentry\Client;
use Sentry\Dsn;
use Sentry\HttpClient\HttpClientFactory;
Expand All @@ -25,6 +26,10 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

use function assert;
use function sprintf;
use function usleep;

class FlushCommand extends Command
{
private HttpClientFactoryInterface $httpClientFactory;
Expand All @@ -45,6 +50,7 @@ protected function configure(): void
{
parent::configure();
$this->addOption('limit-items', null, InputOption::VALUE_REQUIRED, 'How much queue entries should be processed', 60);
$this->addOption('req-per-sec', null, InputOption::VALUE_REQUIRED, 'How many requests per second should be sent', 5);
}

/**
Expand Down Expand Up @@ -84,17 +90,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$requestFactory = Psr17FactoryDiscovery::findRequestFactory();
$sentryClient = Sentry::getInstance()->getClient();

$i = (int)$input->getOption('limit-items');
$option = $input->getOption('limit-items');
assert(is_string($option) || is_int($option));
$reqPerSec = $input->getOption('req-per-sec');
assert(is_string($reqPerSec) || is_int($reqPerSec));
$reqPerSec = (int)$reqPerSec;
$i = (int)$option;
$option = (int)$option;
$output->writeln(sprintf('running with limit-items=%d', $i), $output::VERBOSITY_VERBOSE);
$output->writeln(sprintf('to do: %d queued entries', $this->queue->count() ?? -1), $output::VERBOSITY_VERBOSE);

$lastTime = microtime(true);
do {
$entry = $this->queue->pop();
if (!$entry instanceof Entry) {
break;
}

$i--;
$itemIndex = $input->getOption('limit-items') - $i;
$itemIndex = $option - $i;
$output->writeln(sprintf('start with entry %d', $itemIndex), $output::VERBOSITY_VERBOSE);

$dsn = Dsn::createFromString($entry->getDsn());
Expand All @@ -112,15 +126,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int
try {
$response = $client->sendAsyncRequest($request)->wait();
// fallback for then sendRequest is not throwing ClientErrorException
if ($response->getStatusCode() >= 400) {
if ($response instanceof ResponseInterface && $response->getStatusCode() >= 400) {
throw RequestException::create($request, $response);
}
} catch (ClientException | ClientErrorException $clientErrorException) {
$output->writeln(sprintf('<error>could not send to sentry: %s</error>', $clientErrorException->getMessage()), $output::VERBOSITY_QUIET);
$sentryClient && $sentryClient->captureException($clientErrorException);
if ($clientErrorException->getResponse()->getStatusCode() === 429) {
$output->writeln('<error>Rate limit reached, waiting for sentry to recover sleep(1s)</error>', $output::VERBOSITY_QUIET);
sleep(1); // wait for sentry to recover
Comment thread
Kanti marked this conversation as resolved.
Outdated
}
}

$output->writeln(sprintf('done with at %d', $itemIndex), $output::VERBOSITY_VERBOSE);
if ($i % $reqPerSec === 0) {
$toSleep = max(0, (int)(1_000_000 - (microtime(true) - $lastTime) * 1_000_000));
if ($toSleep) {
$output->writeln(sprintf('%d req/s (sleep %dms)', $reqPerSec, $toSleep / 1_000), $output::VERBOSITY_VERBOSE);
usleep($toSleep);
}

$lastTime = microtime(true);
}
} while ($i > 0);

$output->writeln('<info>done</info>', $output::VERBOSITY_VERBOSE);
Expand Down
29 changes: 16 additions & 13 deletions Classes/Handler/ContentObjectProductionExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,31 @@

use Exception;
use Pluswerk\Sentry\Service\ConfigService;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Throwable;
use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
use Pluswerk\Sentry\Service\Sentry;
use Sentry\SentrySdk;
use Sentry\State\Scope;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Crypto\Random;

class ContentObjectProductionExceptionHandler extends ProductionExceptionHandler
class ContentObjectProductionExceptionHandler implements ExceptionHandlerInterface
{
public function __construct(
Context $context,
Random $random,
LoggerInterface $logger,
protected ProductionExceptionHandler $productionExceptionHandler,
protected ConfigService $configService,
) {
parent::__construct($context, $random, $logger);
}

/**
* @param AbstractContentObject|null $contentObject
* @param array<string, mixed> $contentObjectConfiguration
* @throws Exception
*/
public function handle(Exception $exception, AbstractContentObject $contentObject = null, $contentObjectConfiguration = []): string
public function handle(Exception $exception, ?AbstractContentObject $contentObject = null, $contentObjectConfiguration = []): string
{
// if parent class rethrows the exception the ProductionExceptionHandler will handle the Exception
$result = parent::handle($exception, $contentObject, $contentObjectConfiguration);
$result = $this->productionExceptionHandler->handle($exception, $contentObject, $contentObjectConfiguration);

$oopsCode = $this->getOopsCodeFromResult($result);
try {
Expand All @@ -47,13 +42,13 @@ public function handle(Exception $exception, AbstractContentObject $contentObjec
return $result . $this->getLink($oopsCode);
}

public function getOopsCodeFromResult(string $result): string
private function getOopsCodeFromResult(string $result): string
{
$explode = explode(' ', $result);
return $explode[array_key_last($explode)];
}

public function getLink(string $oopsCode): string
private function getLink(string $oopsCode): string
{
$dsn = SentrySdk::getCurrentHub()->getClient()?->getOptions()->getDsn();
if (!$dsn) {
Expand All @@ -67,4 +62,12 @@ public function getLink(string $oopsCode): string
$url = $schema . '://' . $host . '/organizations/' . $organizationName . '/issues/?project=' . $projectId . '&query=oops_code%3A' . $oopsCode;
return '<a target="_blank" href="' . $url . '" style="text-decoration: none !important;">&nbsp;</a>';
}

/**
* @param array<array-key, mixed> $configuration
*/
public function setConfiguration(array $configuration): void
{
$this->productionExceptionHandler->setConfiguration($configuration);
}
}
12 changes: 12 additions & 0 deletions Classes/Queue/FileQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;

use function is_array;

class FileQueue implements QueueInterface
{
private string $directory;
Expand All @@ -26,6 +28,12 @@ public function __construct(private int $limit = 10000, private bool $compress =
}
}

public function count(): int
{
$iterator = new FilesystemIterator($this->directory, FilesystemIterator::SKIP_DOTS);
return iterator_count($iterator);
}

/**
* @throws JsonException
*/
Expand Down Expand Up @@ -73,6 +81,10 @@ public function pop(): ?Entry
return null;
}

if (!is_array($data)) {
return null;
}

if (!isset($data['dsn'], $data['isEnvelope'], $data['payload'])) {
return null;
}
Expand Down
2 changes: 2 additions & 0 deletions Classes/Queue/QueueInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

interface QueueInterface
{
public function count(): ?int;

public function pop(): ?Entry;

public function push(Entry $entry): void;
Expand Down
9 changes: 7 additions & 2 deletions Classes/Service/ConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ private function getEnv(string $env): ?string
private function getConfig(string $path): ?string
{
try {
return $this->configuration->get('sentry', $path) ?: null;
$config = $this->configuration->get('sentry', $path);
if (!is_string($config)) {
return null;
}

return $config ?: null;
} catch (ExtensionConfigurationPathDoesNotExistException) {
return null;
}
Expand Down Expand Up @@ -60,7 +65,7 @@ public function isEnabled(): bool
return !$this->isDisabled();
}

public function getErrorsToReport(): ?int
public function getErrorsToReport(): int
{
return (int)(
$this->getEnv('SENTRY_ERRORS_TO_REPORT')
Expand Down
25 changes: 17 additions & 8 deletions Classes/Service/ScopeConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected function getExtras(): array
}

/**
* @return array{username: string, id?: string, email?: string}|array{}
* @return array{username: string, id: non-falsy-string, email: non-falsy-string}|array{username: string, id: non-falsy-string}|array{username: string}|array{}
*/
protected function getUserContext(): array
{
Expand All @@ -64,16 +64,25 @@ protected function getUserContext(): array
$userAuthentication = $GLOBALS['BE_USER'] ?? null;
}

if (!$username || !is_string($username)) {
return [];
}

$user = [];
if ($username) {
$user['username'] = $username;
if ($userAuthentication instanceof AbstractUserAuthentication && is_array($userAuthentication->user)) {
$user['id'] = $userAuthentication->user_table . ':' . ($userAuthentication->user['uid'] ?? null);
$user['email'] = $userAuthentication->user['email'] ?? null;
}
$user['username'] = $username;
if (!$userAuthentication instanceof AbstractUserAuthentication || !is_array($userAuthentication->user)) {
return $user;
}

$user['id'] = $userAuthentication->user_table . ':' . ($userAuthentication->user['uid'] ?? null);

$email = $userAuthentication->user['email'] ?? null;
if (!$email) {
return $user;
}

return array_filter($user);
$user['email'] = $email;
return $user;
}

protected function getApplicationType(): ?ApplicationType
Expand Down
2 changes: 1 addition & 1 deletion Classes/Service/Sentry.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public function getHub(): ?HubInterface
return SentrySdk::getCurrentHub();
}

public function withScope(Throwable $exception, callable $withScope = null): void
public function withScope(Throwable $exception, ?callable $withScope = null): void
{
$withScope ??= static fn(Scope $scope) => null;
withScope(
Expand Down
4 changes: 4 additions & 0 deletions Configuration/Services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ services:
command: 'pluswerk:sentry:flush'
description: 'Transports potentially queued events'

pluswerk.sentry.original.contentObject.productionExceptionHandler:
class: TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler
TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler:
class: Pluswerk\Sentry\Handler\ContentObjectProductionExceptionHandler
public: true
shared: false
arguments:
$productionExceptionHandler: '@pluswerk.sentry.original.contentObject.productionExceptionHandler'
10 changes: 5 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"docs": "https://github.com/pluswerk/sentry/blob/master/README.md"
},
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"ext-fileinfo": "*",
"composer-runtime-api": "^2",
"http-interop/http-factory-guzzle": "^1.0",
Expand All @@ -20,10 +20,10 @@
"typo3/cms-frontend": "^11.5 || ^12.4"
},
"require-dev": {
"pluswerk/grumphp-config": "^7",
"saschaegerer/phpstan-typo3": "^1.10.0",
"ssch/typo3-rector": "^2.4.0",
"symfony/http-client": "^5.4 || ^6.4"
"pluswerk/grumphp-config": "^7.2.0",
"saschaegerer/phpstan-typo3": "^1.10.2",
"ssch/typo3-rector": "^2.13.1",
"symfony/http-client": "^5.4 || ^6.4.19"
},
"autoload": {
"psr-4": {
Expand Down
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ includes:
- vendor/andersundsehr/phpstan-git-files/extension.php

parameters:
level: 8
level: max
reportUnmatchedIgnoredErrors: false