Skip to content

Commit

Permalink
Fixed the problem with mapping sentry events. Additionally, limited t…
Browse files Browse the repository at this point in the history
…he number of frames in the exception.
  • Loading branch information
butschster committed Apr 29, 2024
1 parent 2f0b317 commit c1bcd40
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 4 deletions.
29 changes: 28 additions & 1 deletion app/modules/Sentry/Application/Mapper/EventTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,43 @@

final readonly class EventTypeMapper implements EventTypeMapperInterface
{
public function __construct(
public int $maxExceptions = 3,
) {
}

public function toPreview(string $type, array|\JsonSerializable $payload): array|\JsonSerializable
{
$data = $payload instanceof \JsonSerializable ? $payload->jsonSerialize() : $payload;

return [
'exception' => $data['exception'],
'message' => $data['message'] ?? null,
'exception' => $this->limitExceptionFramesNumber(
exception: $data['exception'] ?? null,
max: $this->maxExceptions,
),
'level' => $data['level'] ?? null,
'platform' => $data['platform'],
'environment' => $data['environment'],
'server_name' => $data['server_name'],
'event_id' => $data['event_id'] ?? null,
];
}

public function limitExceptionFramesNumber(array|null $exception, int $max = 3): array|null
{
if ($exception === null) {
return null;
}

foreach ($exception['values'] as $i => $e) {
if (!isset($e['stacktrace']['frames'])) {
continue;
}

$exception['values'][$i]['stacktrace']['frames'] = \array_slice($e['stacktrace']['frames'], -$max);
}

return $exception;
}
}
6 changes: 5 additions & 1 deletion app/modules/Sentry/Interfaces/Http/Handler/EventHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ private function handleEvent(array $data, EventType $eventType): void
$event = $this->handler->handle($data[0]);

$this->commands->dispatch(
new HandleReceivedEvent(type: 'sentry', payload: $event, project: $eventType->project),
new HandleReceivedEvent(
type: 'sentry',
payload: $event,
project: $eventType->project,
),
);
}

Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@
"phpunit/phpunit": "^10.0",
"qossmic/deptrac-shim": "^1.0",
"rector/rector": "^1.0",
"spiral/testing": "^2.6",
"sentry/sentry": "^4.7",
"spiral-packages/database-seeder": "^3.1",
"spiral/testing": "^2.6",
"vimeo/psalm": "^5.16"
},
"autoload": {
Expand Down
150 changes: 149 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions tests/App/Sentry/FakeTransport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Tests\App\Sentry;

use Sentry\Event;
use Sentry\EventId;
use Sentry\Serializer\PayloadSerializerInterface;
use Sentry\Transport\Result;
use Sentry\Transport\ResultStatus;
use Sentry\Transport\TransportInterface;

final class FakeTransport implements TransportInterface
{
/** @var array<non-empty-string, non-empty-string> */
private array $events = [];

public function __construct(
private readonly PayloadSerializerInterface $payloadSerializer,
) {
}

public function send(Event $event): Result
{
$this->events[(string)$event->getId()] = $this->payloadSerializer->serialize($event);

return new Result(ResultStatus::success(), $event);
}

public function close(?int $timeout = null): Result
{
return new Result(ResultStatus::success());
}

public function findEvent(EventId $id): string
{
return $this->events[(string)$id] ?? throw new \InvalidArgumentException('Event not found');
}
}
35 changes: 35 additions & 0 deletions tests/Feature/Interfaces/Http/Sentry/SentryControllerTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Interfaces\Http\Sentry;

use Sentry\Client;
use Sentry\Options;
use Sentry\Serializer\PayloadSerializer;
use Sentry\Severity;
use Sentry\State\Scope;
use Tests\App\Sentry\FakeTransport;
use Tests\Feature\Interfaces\Http\ControllerTestCase;

abstract class SentryControllerTestCase extends ControllerTestCase
{
public function getClient(): Client
{
$options = new Options();
return new Client($options, new FakeTransport(new PayloadSerializer($options)));
}

public function makeEventPayload(string $message, ?Severity $level = null): string
{
$client = $this->getClient();

$level = $level ?? Severity::info();

$scope = new Scope();

$eventId = $client->captureMessage(message: $message, level: $level, scope: $scope);

return $client->getTransport()->findEvent($eventId);
}
}
61 changes: 61 additions & 0 deletions tests/Feature/Interfaces/Http/Sentry/SentryEventActionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Tests\Feature\Interfaces\Http\Sentry;

use App\Application\Broadcasting\Channel\EventsChannel;
use Modules\Projects\Domain\Project;
use Modules\Projects\Domain\ValueObject\Key;
use Nyholm\Psr7\Stream;
use Tests\App\Http\ResponseAssertions;

final class SentryEventActionTest extends SentryControllerTestCase
{
private Project $project;

protected function setUp(): void
{
parent::setUp();

$this->project = $this->createProject('default');
}

public function testSendWithoutGzip(): void
{
$payload = $this->makeEventPayload('Hello world');
$this->makeRequest(payload: $payload, project: $this->project->getKey())->assertOk();

$this->broadcastig->assertPushed(new EventsChannel($this->project->getKey()), function (array $data) {
$this->assertSame('event.received', $data['event']);
$this->assertSame('sentry', $data['data']['type']);
$this->assertSame('default', $data['data']['project']);

$this->assertSame('production', $data['data']['payload']['environment']);
$this->assertSame('Hello world', $data['data']['payload']['message']);
$this->assertSame('info', $data['data']['payload']['level']);

$this->assertNotEmpty($data['data']['uuid']);
$this->assertNotEmpty($data['data']['timestamp']);

return true;
});
}

private function makeRequest(
string $payload,
string $secret = 'secret',
string|Key $project = 'default',
): ResponseAssertions {
return $this->http
->postJson(
uri: '/api/' . $project . '/envelope/',
data: Stream::create($payload),
headers: [
'X-Buggregator-Event' => 'sentry',
'Content-Type' => 'application/x-sentry-envelope',
'X-Sentry-Auth' => 'Sentry sentry_version=7, sentry_client=sentry.php/4.0.1, sentry_key=' . $secret,
],
);
}
}
2 changes: 2 additions & 0 deletions tests/Feature/Interfaces/Http/Sentry/SentryV4ActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public function testSendWithoutGzip(): void
$this->assertSame('Test', $data['data']['payload']['server_name']);
$this->assertSame('production', $data['data']['payload']['environment']);

$this->assertCount(3, $data['data']['payload']['exception']['values'][0]['stacktrace']['frames']);

$this->assertNotEmpty($data['data']['uuid']);
$this->assertNotEmpty($data['data']['timestamp']);

Expand Down
2 changes: 2 additions & 0 deletions tests/Unit/Modules/Sentry/EventTypeMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ public function testMapEvent(): void
$data = $event->getPayload()->jsonSerialize();

$this->assertSame([
'message' => null,
'exception' => $data['exception'],
'level' => null,
'platform' => $data['platform'],
'environment' => $data['environment'],
'server_name' => $data['server_name'],
Expand Down

0 comments on commit c1bcd40

Please sign in to comment.