Skip to content

Commit

Permalink
Merge pull request #13 from spiral/feature/code-refactoring
Browse files Browse the repository at this point in the history
Enhancements and Refactoring
  • Loading branch information
butschster committed Dec 27, 2023
2 parents 097ae96 + 08ab2df commit 7a42ffc
Show file tree
Hide file tree
Showing 14 changed files with 452 additions and 60 deletions.
13 changes: 9 additions & 4 deletions composer.json
Expand Up @@ -14,14 +14,14 @@
"spiral/boot": "^3.0",
"spiral/snapshots": "^3.0",
"sentry/sentry": "^4.0",
"psr/http-factory": "^1.0.1",
"psr/http-message": "^1.0.1 || ^2.0",
"php-http/curl-client": "^2.3.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
"vimeo/psalm": "^5.17",
"psr/log": "^3.0",
"spiral/testing": "^2.2"
"spiral/testing": "^2.6"
},
"autoload": {
"psr-4": {
Expand All @@ -34,5 +34,10 @@
}
},
"minimum-stability": "dev",
"prefer-stable": true
"prefer-stable": true,
"config": {
"allow-plugins": {
"php-http/discovery": true
}
}
}
144 changes: 131 additions & 13 deletions src/Bootloader/ClientBootloader.php
Expand Up @@ -6,41 +6,148 @@

use Sentry\ClientBuilder;
use Sentry\ClientInterface;
use Sentry\Integration\RequestFetcherInterface;
use Sentry\Options;
use Sentry\SentrySdk;
use Sentry\State\Hub;
use Sentry\State\HubInterface;
use Sentry\State\Scope;
use Spiral\Boot\Bootloader\Bootloader;
use Spiral\Boot\DirectoriesInterface;
use Spiral\Boot\EnvironmentInterface;
use Spiral\Boot\FinalizerInterface;
use Spiral\Config\ConfiguratorInterface;
use Spiral\Sentry\Config\SentryConfig;
use Spiral\Sentry\Http\RequestScope;
use Spiral\Sentry\Version;
use Sentry\Integration as SdkIntegration;

class ClientBootloader extends Bootloader
{
protected const SINGLETONS = [
ClientInterface::class => [self::class, 'createClient'],
];
/** @var SdkIntegration\IntegrationInterface[] */
private array $integrations = [];

public function __construct(
private readonly ConfiguratorInterface $config
private readonly ConfiguratorInterface $config,
) {
}

public function init(EnvironmentInterface $env): void
public function defineSingletons(): array
{
return [
Options::class => [self::class, 'createOptions'],
HubInterface::class => [self::class, 'createHub'],
ClientInterface::class => [self::class, 'getClient'],
RequestFetcherInterface::class => RequestScope::class,
];
}

/**
* Register a new integration to be added to the Sentry SDK.
*/
public function addIntegration(SdkIntegration\IntegrationInterface $integration): void
{
$this->integrations[] = $integration;
}

public function init(EnvironmentInterface $env, FinalizerInterface $finalizer): void
{
$this->config->setDefaults('sentry', [
'dsn' => \trim($env->get('SENTRY_DSN', ''), "\n\t\r \"'") // typical typos
'dsn' => \trim($env->get('SENTRY_DSN', ''), "\n\t\r \"'"), // typical typos
'environment' => $env->get('SENTRY_ENVIRONMENT') ?? null,
'release' => $env->get('SENTRY_RELEASE') ?? null,
'sample_rate' => $env->get('SENTRY_SAMPLE_RATE') === null
? 1.0
: (float)$env->get('SENTRY_SAMPLE_RATE'),
'traces_sample_rate' => $env->get('SENTRY_TRACES_SAMPLE_RATE') === null
? null
: (float)$env->get('SENTRY_TRACES_SAMPLE_RATE'),
'send_default_pii' => (bool)$env->get('SENTRY_SEND_DEFAULT_PII'),
]);
}

private function createClient(SentryConfig $config): ClientInterface
/**
* Create the Sentry SDK options.
*/
private function createOptions(
SentryConfig $config,
DirectoriesInterface $dirs,
EnvironmentInterface $env,
RequestFetcherInterface $requestScope,
): Options {
$options = new Options([
'dsn' => $config->getDSN(),
'environment' => $config->getEnvironment(),
'release' => $config->getRelease(),
]);

$options->setSampleRate($config->getSampleRate());
$options->setTracesSampleRate($config->getTracesSampleRate());
$options->setSendDefaultPii($config->isSendDefaultPii());

$options->setPrefixes([
$dirs->get('root'),
]);

$options->setInAppExcludedPaths([
$dirs->get('root') . '/vendor',
]);

if ($config->getEnvironment() === null) {
$options->setEnvironment($env->get('APP_ENV'));
}

if ($config->getRelease() === null) {
$options->setRelease($env->get('APP_VERSION'));
}

$options->setIntegrations(function (array $integrations) use ($options, $requestScope): array {
if ($options->hasDefaultIntegrations()) {
// Remove the default error and fatal exception listeners to let Spiral handle those
// itself. These event are still bubbling up through the documented changes in the users
// `ExceptionHandler` of their application or through the log channel integration to Sentry
$integrations = \array_filter(
$integrations,
static function (SdkIntegration\IntegrationInterface $integration): bool {
if ($integration instanceof SdkIntegration\ErrorListenerIntegration) {
return false;
}

if ($integration instanceof SdkIntegration\ExceptionListenerIntegration) {
return false;
}

if ($integration instanceof SdkIntegration\FatalErrorListenerIntegration) {
return false;
}

// We also remove the default request integration so it can be readded
// after with a Laravel specific request fetcher. This way we can resolve
// the request from Laravel instead of constructing it from the global state
if ($integration instanceof SdkIntegration\RequestIntegration) {
return false;
}

return true;
},
);

$integrations[] = new SdkIntegration\RequestIntegration($requestScope);
}

return [...$integrations, ...$this->integrations];
});

return $options;
}

private function createHub(Options $options, FinalizerInterface $finalizer): HubInterface
{
/**
* @psalm-suppress InternalClass
* @psalm-suppress InternalMethod
*/
$builder = ClientBuilder::create([
'dsn' => $config->getDSN(),
]);
$builder = new ClientBuilder($options);

/** @psalm-suppress InternalMethod */
$builder->setSdkIdentifier(Version::SDK_IDENTIFIER);
Expand All @@ -49,10 +156,21 @@ private function createClient(SentryConfig $config): ClientInterface
$builder->setSdkVersion(Version::SDK_VERSION);

/** @psalm-suppress InternalMethod */
$client = $builder->getClient();
$hub = new Hub($builder->getClient());

SentrySdk::setCurrentHub($hub);

SentrySdk::setCurrentHub(new Hub($client));
$finalizer->addFinalizer(static function () use ($hub): void {
$hub->configureScope(function (Scope $scope): void {
$scope->clear();
});
});

return $client;
return $hub;
}

private function getClient(HubInterface $hub): ?ClientInterface
{
return $hub->getClient();
}
}
18 changes: 12 additions & 6 deletions src/Bootloader/SentryBootloader.php
Expand Up @@ -10,11 +10,17 @@

final class SentryBootloader extends Bootloader
{
protected const DEPENDENCIES = [
ClientBootloader::class,
];
public function defineDependencies(): array
{
return [
ClientBootloader::class,
];
}

protected const BINDINGS = [
SnapshotterInterface::class => SentrySnapshotter::class,
];
public function defineBindings(): array
{
return [
SnapshotterInterface::class => SentrySnapshotter::class,
];
}
}
9 changes: 6 additions & 3 deletions src/Bootloader/SentryReporterBootloader.php
Expand Up @@ -11,9 +11,12 @@

final class SentryReporterBootloader extends Bootloader
{
protected const DEPENDENCIES = [
ClientBootloader::class,
];
public function defineDependencies(): array
{
return [
ClientBootloader::class,
];
}

public function init(AbstractKernel $kernel): void
{
Expand Down
22 changes: 11 additions & 11 deletions src/Client.php
Expand Up @@ -7,36 +7,36 @@
use Psr\Container\ContainerInterface;
use Psr\Log\LogLevel;
use Sentry\Breadcrumb;
use Sentry\ClientInterface;
use Sentry\EventId;
use Sentry\State\HubInterface;
use Sentry\State\Scope;
use Spiral\Debug\StateInterface;
use Spiral\Logger\Event\LogEvent;

final class Client
{
public function __construct(
private readonly ClientInterface $client,
private readonly HubInterface $hub,
private readonly ContainerInterface $container,
) {
}

public function send(\Throwable $exception): ?EventId
{
$scope = new Scope();

if ($this->container->has(StateInterface::class)) {
$state = $this->container->get(StateInterface::class);

$scope->setTags($state->getTags());
$scope->setExtras($state->getVariables());
$this->hub->configureScope(function (Scope $scope) use ($state): void {
$scope->setTags($state->getTags());
$scope->setExtras($state->getVariables());

foreach ($state->getLogEvents() as $event) {
$scope->addBreadcrumb($this->makeBreadcrumb($event));
}
foreach ($state->getLogEvents() as $event) {
$scope->addBreadcrumb($this->makeBreadcrumb($event));
}
});
}

return $this->client->captureException($exception, $scope);
return $this->hub->captureException($exception);
}

private function makeBreadcrumb(LogEvent $event): Breadcrumb
Expand Down Expand Up @@ -68,7 +68,7 @@ private function makeBreadcrumb(LogEvent $event): Breadcrumb
'default',
$event->getChannel(),
$event->getMessage(),
$event->getContext()
$event->getContext(),
);
}
}
63 changes: 62 additions & 1 deletion src/Config/SentryConfig.php
Expand Up @@ -15,10 +15,71 @@ final class SentryConfig extends InjectableConfig

protected array $config = [
'dsn' => '',
'environment' => null,
'release' => null,
'sample_rate' => 1.0,
'traces_sample_rate' => null,
'send_default_pii' => false,
];

/**
* @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
*/
public function getDSN(): string
{
return $this->config['dsn'];
}
}

/**
* This string is freeform and set to production by default. A release can be associated with
* more than one environment to separate them in the UI (think staging vs production or similar).
*/
public function getEnvironment(): ?string
{
return $this->config['environment'] ?? $_SERVER['SENTRY_ENVIRONMENT'] ?? null;
}

/**
* The release version of your application.
* Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
*/
public function getRelease(): ?string
{
return $this->config['release'] ?? $_SERVER['SENTRY_RELEASE'] ?? null;
}

/**
* Configures the sample rate for error events, in the range of 0.0 to 1.0. The default is 1.0 which means that
* 100% of error events are sent. If set to 0.1 only 10% of error events will be sent. Events are picked randomly.
*
* @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#sample-rate
*/
public function getSampleRate(): float
{
return $this->config['sample_rate'];
}

/**
* A number between 0 and 1, controlling the percentage chance a given transaction will be sent to Sentry.
* (0 represents 0% while 1 represents 100%.) Applies equally to all transactions created in the app. Either this
* or traces_sampler must be defined to enable tracingThe process of logging the events that took place during a
* request, often across multiple services..
*
* @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate
*/
public function getTracesSampleRate(): ?float
{
return $this->config['traces_sample_rate'];
}

/**
* If this flag is enabled, certain personally identifiable information (PII) is added by active integrations.
* By default, no such data is sent.
*
* @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send-default-pii
*/
public function isSendDefaultPii(): bool
{
return $this->config['send_default_pii'] ?? false;
}
}

0 comments on commit 7a42ffc

Please sign in to comment.