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 all commits
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(5s)</error>', $output::VERBOSITY_QUIET);
sleep(5); // wait for sentry to recover
}
}

$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