From 58553361d2afecabe960ec499f7a6b4f9338afb1 Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 22 Jun 2024 15:19:45 +0400 Subject: [PATCH] Refactors Sentry module: 1. Adds tables for sentry events --- app/config/migration.php | 15 +- ...02036_1_1_default_create_sentry_traces.php | 34 +++++ ...02036_2_2_default_create_sentry_issues.php | 41 ++++++ ...36_3_3_default_create_sentry_issue_tag.php | 34 +++++ ...fault_create_sentry_issue_fingerprints.php | 38 ++++++ ...ex_events_index_group_id_667079a9c5a94.php | 28 ++++ app/modules/Events/Domain/Event.php | 13 ++ .../Domain/EventRepositoryInterface.php | 3 +- .../Integration/CycleOrm/EventRepository.php | 37 ++++- .../Interfaces/Commands/StoreEventHandler.php | 2 + .../Handlers/MergeEventsHandler.php | 2 +- .../Handlers/RemoveSfDumpScriptHandler.php | 10 +- .../Interfaces/Http/Handler/EventHandler.php | 5 +- .../Sentry/Application/DTO/Exception.php | 31 +++++ .../Application/DTO/JavascriptPayload.php | 7 + .../Sentry/Application/DTO/JsonChunk.php | 22 ++- .../Sentry/Application/DTO/MetaChunk.php | 71 ++++++++++ .../Sentry/Application/DTO/PHPPayload.php | 7 + .../Sentry/Application/DTO/Payload.php | 90 ++++++++---- .../Application/DTO/PayloadChunkInterface.php | 5 +- .../Sentry/Application/DTO/PayloadFactory.php | 66 +++++++++ .../Sentry/Application/DTO/Platform.php | 41 ++++++ app/modules/Sentry/Application/DTO/Type.php | 1 + .../Sentry/Application/DTO/VueJsPayload.php | 7 + .../Application/EventHandlerInterface.php | 5 +- .../Handlers/StoreEventHandler.php | 82 +++++++++++ .../Handlers/StoreTraceHandler.php | 105 ++++++++++++++ .../Application/Mapper/EventTypeMapper.php | 2 + .../Sentry/Application/PayloadParser.php | 35 ++--- .../Sentry/Application/SentryBootloader.php | 50 ++++++- app/modules/Sentry/Domain/Fingerprint.php | 55 ++++++++ .../Domain/FingerprintFactoryInterface.php | 16 +++ .../Domain/FingerprintRepositoryInterface.php | 18 +++ app/modules/Sentry/Domain/Issue.php | 129 ++++++++++++++++++ .../Sentry/Domain/IssueFactoryInterface.php | 16 +++ .../Domain/IssueRepositoryInterface.php | 12 ++ app/modules/Sentry/Domain/IssueTag.php | 46 +++++++ .../Domain/IssueTagRepositoryInterface.php | 12 ++ app/modules/Sentry/Domain/Trace.php | 91 ++++++++++++ .../Sentry/Domain/TraceFactoryInterface.php | 16 +++ .../Domain/TraceRepositoryInterface.php | 12 ++ app/modules/Sentry/Domain/ValueObject/Sdk.php | 12 ++ app/modules/Sentry/EventHandler.php | 8 +- .../CycleOrm/FingerprintFactory.php | 28 ++++ .../CycleOrm/FingerprintRepository.php | 78 +++++++++++ .../Integration/CycleOrm/IssueFactory.php | 45 ++++++ .../Integration/CycleOrm/IssueRepository.php | 21 +++ .../CycleOrm/IssueTagRepository.php | 13 ++ .../Integration/CycleOrm/TraceFactory.php | 29 ++++ .../Integration/CycleOrm/TraceRepository.php | 13 ++ .../Interfaces/Http/Handler/EventHandler.php | 39 +----- .../Http/Handler/JsEventHandler.php | 33 +---- .../Interfaces/Http/ShowIssueAction.php | 38 ++++++ .../Interfaces/Http/ShowIssueStatAction.php | 42 ++++++ .../Commands/HandleReceivedEvent.php | 4 + .../Application/Domain/ValueObjects/Json.php | 6 +- app/src/Application/Event/StackStrategy.php | 12 ++ app/src/Application/HTTP/GzippedStream.php | 15 +- .../Http/Sentry/SentryV3ActionTest.php | 90 ------------ .../Http/Sentry/SentryV4ActionTest.php | 2 +- .../Http/Sentry/SentryVueEventActionTest.php | 1 + 61 files changed, 1587 insertions(+), 254 deletions(-) create mode 100644 app/database/Migrations/20240616.102036_1_1_default_create_sentry_traces.php create mode 100644 app/database/Migrations/20240616.102036_2_2_default_create_sentry_issues.php create mode 100644 app/database/Migrations/20240616.102036_3_3_default_create_sentry_issue_tag.php create mode 100644 app/database/Migrations/20240616.102036_4_4_default_create_sentry_issue_fingerprints.php create mode 100644 app/database/Migrations/20240617.180009_1_1_default_change_events_add_group_id_add_index_events_index_group_id_667079a9c5a94.php create mode 100644 app/modules/Sentry/Application/DTO/Exception.php create mode 100644 app/modules/Sentry/Application/DTO/JavascriptPayload.php create mode 100644 app/modules/Sentry/Application/DTO/MetaChunk.php create mode 100644 app/modules/Sentry/Application/DTO/PHPPayload.php create mode 100644 app/modules/Sentry/Application/DTO/PayloadFactory.php create mode 100644 app/modules/Sentry/Application/DTO/Platform.php create mode 100644 app/modules/Sentry/Application/DTO/VueJsPayload.php create mode 100644 app/modules/Sentry/Application/Handlers/StoreEventHandler.php create mode 100644 app/modules/Sentry/Application/Handlers/StoreTraceHandler.php create mode 100644 app/modules/Sentry/Domain/Fingerprint.php create mode 100644 app/modules/Sentry/Domain/FingerprintFactoryInterface.php create mode 100644 app/modules/Sentry/Domain/FingerprintRepositoryInterface.php create mode 100644 app/modules/Sentry/Domain/Issue.php create mode 100644 app/modules/Sentry/Domain/IssueFactoryInterface.php create mode 100644 app/modules/Sentry/Domain/IssueRepositoryInterface.php create mode 100644 app/modules/Sentry/Domain/IssueTag.php create mode 100644 app/modules/Sentry/Domain/IssueTagRepositoryInterface.php create mode 100644 app/modules/Sentry/Domain/Trace.php create mode 100644 app/modules/Sentry/Domain/TraceFactoryInterface.php create mode 100644 app/modules/Sentry/Domain/TraceRepositoryInterface.php create mode 100644 app/modules/Sentry/Domain/ValueObject/Sdk.php create mode 100644 app/modules/Sentry/Integration/CycleOrm/FingerprintFactory.php create mode 100644 app/modules/Sentry/Integration/CycleOrm/FingerprintRepository.php create mode 100644 app/modules/Sentry/Integration/CycleOrm/IssueFactory.php create mode 100644 app/modules/Sentry/Integration/CycleOrm/IssueRepository.php create mode 100644 app/modules/Sentry/Integration/CycleOrm/IssueTagRepository.php create mode 100644 app/modules/Sentry/Integration/CycleOrm/TraceFactory.php create mode 100644 app/modules/Sentry/Integration/CycleOrm/TraceRepository.php create mode 100644 app/modules/Sentry/Interfaces/Http/ShowIssueAction.php create mode 100644 app/modules/Sentry/Interfaces/Http/ShowIssueStatAction.php create mode 100644 app/src/Application/Event/StackStrategy.php delete mode 100644 tests/Feature/Interfaces/Http/Sentry/SentryV3ActionTest.php diff --git a/app/config/migration.php b/app/config/migration.php index 1d88f361..3a41901b 100644 --- a/app/config/migration.php +++ b/app/config/migration.php @@ -2,21 +2,16 @@ declare(strict_types=1); +use Cycle\Schema\Generator\Migrations\Strategy\MultipleFilesStrategy; + return [ - /** - * Directory to store migration files - */ 'directory' => directory('app') . 'database/Migrations/', - /** - * Table name to store information about migrations status (per database) - */ 'table' => 'migrations', - 'strategy' => \Cycle\Schema\Generator\Migrations\Strategy\MultipleFilesStrategy::class, + 'strategy' => MultipleFilesStrategy::class, - /** - * When set to true no confirmation will be requested on migration run. - */ 'safe' => true, + + 'namespace' => 'Database\Migrations', ]; diff --git a/app/database/Migrations/20240616.102036_1_1_default_create_sentry_traces.php b/app/database/Migrations/20240616.102036_1_1_default_create_sentry_traces.php new file mode 100644 index 00000000..cb312337 --- /dev/null +++ b/app/database/Migrations/20240616.102036_1_1_default_create_sentry_traces.php @@ -0,0 +1,34 @@ +table('sentry_traces') + ->addColumn('uuid', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 36]) + ->addColumn('trace_id', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 32]) + ->addColumn('public_key', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 255]) + ->addColumn('environment', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 255]) + ->addColumn('sampled', 'boolean', ['nullable' => false, 'defaultValue' => null]) + ->addColumn('sample_rate', 'float', ['nullable' => false, 'defaultValue' => null]) + ->addColumn('transaction', 'string', ['nullable' => true, 'defaultValue' => null, 'size' => 255]) + ->addColumn('sdk', 'jsonb', ['nullable' => false, 'defaultValue' => null]) + ->addColumn('language', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 255]) + ->addIndex(['trace_id'], ['name' => 'sentry_traces_index_trace_id_666ebc74b7a32', 'unique' => true]) + ->setPrimaryKeys(['uuid']) + ->create(); + } + + public function down(): void + { + $this->table('sentry_traces')->drop(); + } +} diff --git a/app/database/Migrations/20240616.102036_2_2_default_create_sentry_issues.php b/app/database/Migrations/20240616.102036_2_2_default_create_sentry_issues.php new file mode 100644 index 00000000..59b3fe1c --- /dev/null +++ b/app/database/Migrations/20240616.102036_2_2_default_create_sentry_issues.php @@ -0,0 +1,41 @@ +table('sentry_issues') + ->addColumn('created_at', 'datetime', ['nullable' => false, 'defaultValue' => null, 'withTimezone' => false]) + ->addColumn('uuid', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 36]) + ->addColumn('trace_uuid', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 36]) + ->addColumn('title', 'text', ['nullable' => false, 'defaultValue' => null]) + ->addColumn('platform', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 32]) + ->addColumn('logger', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 32]) + ->addColumn('type', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 32]) + ->addColumn('transaction', 'string', ['nullable' => true, 'defaultValue' => null, 'size' => 255]) + ->addColumn('server_name', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 255]) + ->addColumn('payload', 'jsonb', ['nullable' => false, 'defaultValue' => null]) + ->addIndex(['trace_uuid'], ['name' => 'sentry_issues_index_trace_uuid_666ebc74b7900', 'unique' => false]) + ->addForeignKey(['trace_uuid'], 'sentry_traces', ['uuid'], [ + 'name' => 'sentry_issues_foreign_trace_uuid_666ebc74b78fb', + 'delete' => 'CASCADE', + 'update' => 'CASCADE', + 'indexCreate' => true, + ]) + ->setPrimaryKeys(['uuid']) + ->create(); + } + + public function down(): void + { + $this->table('sentry_issues')->drop(); + } +} diff --git a/app/database/Migrations/20240616.102036_3_3_default_create_sentry_issue_tag.php b/app/database/Migrations/20240616.102036_3_3_default_create_sentry_issue_tag.php new file mode 100644 index 00000000..a16598dd --- /dev/null +++ b/app/database/Migrations/20240616.102036_3_3_default_create_sentry_issue_tag.php @@ -0,0 +1,34 @@ +table('sentry_issue_tag') + ->addColumn('issue_uuid', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 36]) + ->addColumn('tag', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 255]) + ->addColumn('value', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 255]) + ->addIndex(['issue_uuid'], ['name' => 'sentry_issue_tag_index_issue_uuid_666ebc74b7863', 'unique' => false]) + ->addForeignKey(['issue_uuid'], 'sentry_issues', ['uuid'], [ + 'name' => 'sentry_issue_tag_foreign_issue_uuid_666ebc74b7870', + 'delete' => 'CASCADE', + 'update' => 'CASCADE', + 'indexCreate' => true, + ]) + ->setPrimaryKeys(['issue_uuid', 'tag']) + ->create(); + } + + public function down(): void + { + $this->table('sentry_issue_tag')->drop(); + } +} diff --git a/app/database/Migrations/20240616.102036_4_4_default_create_sentry_issue_fingerprints.php b/app/database/Migrations/20240616.102036_4_4_default_create_sentry_issue_fingerprints.php new file mode 100644 index 00000000..1f5ede6e --- /dev/null +++ b/app/database/Migrations/20240616.102036_4_4_default_create_sentry_issue_fingerprints.php @@ -0,0 +1,38 @@ +table('sentry_issue_fingerprints') + ->addColumn('created_at', 'datetime', ['nullable' => false, 'defaultValue' => null, 'withTimezone' => false], + ) + ->addColumn('uuid', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 36]) + ->addColumn('issue_uuid', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 36]) + ->addColumn('fingerprint', 'string', ['nullable' => false, 'defaultValue' => null, 'size' => 50]) + ->addIndex(['issue_uuid'], + ['name' => 'sentry_issue_fingerprints_index_issue_uuid_666ebc74b7929', 'unique' => false]) + ->addIndex(['issue_uuid', 'fingerprint'], ['name' => '9961459aa46305dec16ff24fb1284ae6', 'unique' => true]) + ->addForeignKey(['issue_uuid'], 'sentry_issues', ['uuid'], [ + 'name' => 'bad38aad05e5c71fac6b43c8f4ef8066', + 'delete' => 'CASCADE', + 'update' => 'CASCADE', + 'indexCreate' => true, + ]) + ->setPrimaryKeys(['uuid']) + ->create(); + } + + public function down(): void + { + $this->table('sentry_issue_fingerprints')->drop(); + } +} diff --git a/app/database/Migrations/20240617.180009_1_1_default_change_events_add_group_id_add_index_events_index_group_id_667079a9c5a94.php b/app/database/Migrations/20240617.180009_1_1_default_change_events_add_group_id_add_index_events_index_group_id_667079a9c5a94.php new file mode 100644 index 00000000..239d6bab --- /dev/null +++ b/app/database/Migrations/20240617.180009_1_1_default_change_events_add_group_id_add_index_events_index_group_id_667079a9c5a94.php @@ -0,0 +1,28 @@ +table('events') + ->addColumn('group_id', 'string', ['nullable' => true, 'defaultValue' => null, 'size' => 255]) + ->addIndex(['group_id'], ['name' => 'events_index_group_id_667079a9c5a94', 'unique' => false]) + ->update(); + } + + public function down(): void + { + $this->table('events') + ->dropIndex(['group_id']) + ->dropColumn('group_id') + ->update(); + } +} diff --git a/app/modules/Events/Domain/Event.php b/app/modules/Events/Domain/Event.php index a07e4f3c..617d5991 100644 --- a/app/modules/Events/Domain/Event.php +++ b/app/modules/Events/Domain/Event.php @@ -17,6 +17,7 @@ )] #[Index(columns: ['type'])] #[Index(columns: ['project'])] +#[Index(columns: ['group_id'])] class Event { /** @internal */ @@ -29,6 +30,8 @@ public function __construct( private Json $payload, #[Column(type: 'string(25)', typecast: Timestamp::class)] private Timestamp $timestamp, + #[Column(type: 'string', name: 'group_id', nullable: true)] + private ?string $groupId = null, #[Column(type: 'string', nullable: true, typecast: Key::class)] private ?Key $project = null, ) {} @@ -58,8 +61,18 @@ public function getTimestamp(): Timestamp return $this->timestamp; } + public function updateTimestamp(?Timestamp $timestamp = null): void + { + $this->timestamp = $timestamp ?? Timestamp::create(); + } + public function getProject(): ?Key { return $this->project; } + + public function getGroupId(): ?string + { + return $this->groupId; + } } diff --git a/app/modules/Events/Domain/EventRepositoryInterface.php b/app/modules/Events/Domain/EventRepositoryInterface.php index e40eaae7..d307a7f8 100644 --- a/app/modules/Events/Domain/EventRepositoryInterface.php +++ b/app/modules/Events/Domain/EventRepositoryInterface.php @@ -4,6 +4,7 @@ namespace Modules\Events\Domain; +use App\Application\Event\StackStrategy; use Cycle\ORM\RepositoryInterface; /** @@ -16,7 +17,7 @@ public function findAll(array $scope = [], array $orderBy = [], int $limit = 30, public function countAll(array $scope = []): int; - public function store(Event $event): bool; + public function store(Event $event, StackStrategy $stackStrategy): bool; public function deleteAll(array $scope = []): void; diff --git a/app/modules/Events/Integration/CycleOrm/EventRepository.php b/app/modules/Events/Integration/CycleOrm/EventRepository.php index e8c7f2c0..7006293e 100644 --- a/app/modules/Events/Integration/CycleOrm/EventRepository.php +++ b/app/modules/Events/Integration/CycleOrm/EventRepository.php @@ -4,6 +4,7 @@ namespace Modules\Events\Integration\CycleOrm; +use App\Application\Event\StackStrategy; use Cycle\ORM\EntityManagerInterface; use Cycle\ORM\Select; use Cycle\ORM\Select\Repository; @@ -23,10 +24,33 @@ public function __construct( parent::__construct($select); } - public function store(Event $event): bool + public function store(Event $event, StackStrategy $stackStrategy): bool { - if ($found = $this->findByPK($event->getUuid())) { - $found->setPayload($event->getPayload()); + $found = null; + if ($event->getGroupId() !== null && $stackStrategy === StackStrategy::All) { + $found = $this->findOne(['group_id' => $event->getGroupId()]); + if (!$found) { + $found = $event; + } else { + $found->setPayload($event->getPayload()); + $found->updateTimestamp(); + } + } elseif ($event->getGroupId() !== null && $stackStrategy === StackStrategy::OnlyLatest) { + $found = $this->findLatest(); + if ($found && $found->getGroupId() === $event->getGroupId()) { + $found->setPayload($event->getPayload()); + $found->updateTimestamp(); + } else { + $found = $event; + } + } + +// if (!$found && $found = $this->findByPK($event->getUuid())) { +// $found->setPayload($event->getPayload()); +// $found->updateTimestamp(); +// } + + if ($found) { $this->em->persist($found); } else { $this->em->persist($event); @@ -98,4 +122,11 @@ private function buildScope(array $scope): array return $newScope; } + + private function findLatest(): ?Event + { + return $this->select() + ->orderBy(['timestamp' => 'DESC']) + ->fetchOne(); + } } diff --git a/app/modules/Events/Interfaces/Commands/StoreEventHandler.php b/app/modules/Events/Interfaces/Commands/StoreEventHandler.php index 012ac9bd..33187f71 100644 --- a/app/modules/Events/Interfaces/Commands/StoreEventHandler.php +++ b/app/modules/Events/Interfaces/Commands/StoreEventHandler.php @@ -40,8 +40,10 @@ public function handle(HandleReceivedEvent $command): void type: $command->type, payload: new Json($command->payload), timestamp: Timestamp::create(), + groupId: $command->groupId, project: $project?->getKey(), ), + $command->stackStrategy, ); $this->dispatcher->dispatch( diff --git a/app/modules/Ray/Application/Handlers/MergeEventsHandler.php b/app/modules/Ray/Application/Handlers/MergeEventsHandler.php index 09619b36..206f3c80 100644 --- a/app/modules/Ray/Application/Handlers/MergeEventsHandler.php +++ b/app/modules/Ray/Application/Handlers/MergeEventsHandler.php @@ -7,7 +7,7 @@ use App\Application\Commands\FindEventByUuid; use App\Application\Domain\ValueObjects\Uuid; use App\Application\Exception\EntityNotFoundException; -use Modules\Sentry\Application\EventHandlerInterface; +use Modules\Ray\Application\EventHandlerInterface; use Spiral\Cqrs\QueryBusInterface; final readonly class MergeEventsHandler implements EventHandlerInterface diff --git a/app/modules/Ray/Application/Handlers/RemoveSfDumpScriptHandler.php b/app/modules/Ray/Application/Handlers/RemoveSfDumpScriptHandler.php index b67c5fcc..e44fc0f6 100644 --- a/app/modules/Ray/Application/Handlers/RemoveSfDumpScriptHandler.php +++ b/app/modules/Ray/Application/Handlers/RemoveSfDumpScriptHandler.php @@ -5,7 +5,7 @@ namespace Modules\Ray\Application\Handlers; use Modules\Ray\Application\DumpIdParser; -use Modules\Sentry\Application\EventHandlerInterface; +use Modules\Ray\Application\EventHandlerInterface; final class RemoveSfDumpScriptHandler implements EventHandlerInterface { @@ -45,9 +45,9 @@ private function cleanHtml(string $html): string // Remove everything except
 tags and their content
         return \preg_replace(
-            '/(?s)(.*?)(]*>.*?<\/pre>)(.*)|(?s)(.*)/',
-            '$2',
-            $html,
-        ) . '';
+                '/(?s)(.*?)(]*>.*?<\/pre>)(.*)|(?s)(.*)/',
+                '$2',
+                $html,
+            ) . '';
     }
 }
diff --git a/app/modules/Ray/Interfaces/Http/Handler/EventHandler.php b/app/modules/Ray/Interfaces/Http/Handler/EventHandler.php
index 0a372110..4f1b33bb 100644
--- a/app/modules/Ray/Interfaces/Http/Handler/EventHandler.php
+++ b/app/modules/Ray/Interfaces/Http/Handler/EventHandler.php
@@ -8,6 +8,7 @@
 use App\Application\Commands\HandleReceivedEvent;
 use App\Application\Domain\ValueObjects\Uuid;
 use App\Application\Event\EventType;
+use App\Application\Event\StackStrategy;
 use App\Application\Service\HttpHandler\HandlerInterface;
 use Carbon\CarbonInterval;
 use Modules\Ray\Application\EventHandlerInterface;
@@ -70,7 +71,9 @@ private function handleEvent(ServerRequestInterface $request, EventType $eventTy
                 type: $eventType->type,
                 payload: $event,
                 project: $eventType->project,
-                uuid: Uuid::fromString($event['uuid']),
+//                uuid: Uuid::fromString($event['uuid']),
+                groupId: $event['uuid'],
+                stackStrategy: StackStrategy::All,
             ),
         );
 
diff --git a/app/modules/Sentry/Application/DTO/Exception.php b/app/modules/Sentry/Application/DTO/Exception.php
new file mode 100644
index 00000000..7789c569
--- /dev/null
+++ b/app/modules/Sentry/Application/DTO/Exception.php
@@ -0,0 +1,31 @@
+exception['value'] ?? null;
+    }
+
+    public function type(): ?string
+    {
+        return $this->exception['type'] ?? null;
+    }
+
+    public function calculateFingerprint(): string
+    {
+        $string = $this->message() . $this->type();
+
+        foreach ($this->exception['stacktrace']['frames'] as $frame) {
+            $string .= $frame['filename'] . $frame['lineno'] . ($frame['context_line'] ?? '');
+        }
+
+        return \md5($string);
+    }
+}
diff --git a/app/modules/Sentry/Application/DTO/JavascriptPayload.php b/app/modules/Sentry/Application/DTO/JavascriptPayload.php
new file mode 100644
index 00000000..2be12ffa
--- /dev/null
+++ b/app/modules/Sentry/Application/DTO/JavascriptPayload.php
@@ -0,0 +1,7 @@
+data;
     }
+
+    public function offsetExists(mixed $offset): bool
+    {
+        return isset($this->data[$offset]);
+    }
+
+    public function offsetGet(mixed $offset): mixed
+    {
+        return $this->data[$offset];
+    }
+
+    public function offsetSet(mixed $offset, mixed $value): void
+    {
+        throw new \BadMethodCallException('JsonChunk is readonly');
+    }
+
+    public function offsetUnset(mixed $offset): void
+    {
+        throw new \BadMethodCallException('JsonChunk is readonly');
+    }
 }
diff --git a/app/modules/Sentry/Application/DTO/MetaChunk.php b/app/modules/Sentry/Application/DTO/MetaChunk.php
new file mode 100644
index 00000000..4c03f795
--- /dev/null
+++ b/app/modules/Sentry/Application/DTO/MetaChunk.php
@@ -0,0 +1,71 @@
+data['event_id'] ?? (string) Uuid::generate();
+    }
+
+    public function traceId(): string
+    {
+        return $this->data['trace']['trace_id'] ?? (string) Uuid::generate();
+    }
+
+    public function publicKey(): string
+    {
+        return $this->data['trace']['public_key'] ?? '';
+    }
+
+    public function environment(): string
+    {
+        return $this->data['trace']['environment'] ?? '';
+    }
+
+    public function platform(): Platform
+    {
+        $sdk = $this->data['sdk'];
+
+        return Platform::detect($sdk['name']);
+    }
+
+    public function sampled(): bool
+    {
+        if (!isset($this->data['trace']['sampled'])) {
+            return false;
+        }
+
+        $value = $this->data['trace']['sampled'];
+
+        if (\is_bool($value)) {
+            return $value;
+        }
+
+        if (\is_string($value)) {
+            return $value === 'true';
+        }
+
+        return false;
+    }
+
+    public function sampleRate(): float
+    {
+        return (float) ($this->data['trace']['sample_rate'] ?? 0.0);
+    }
+
+    public function transaction(): ?string
+    {
+        return $this->data['trace']['transaction'] ?? null;
+    }
+
+    public function sdk(): array
+    {
+        return $this->data['sdk'] ?? [];
+    }
+}
diff --git a/app/modules/Sentry/Application/DTO/PHPPayload.php b/app/modules/Sentry/Application/DTO/PHPPayload.php
new file mode 100644
index 00000000..de757b17
--- /dev/null
+++ b/app/modules/Sentry/Application/DTO/PHPPayload.php
@@ -0,0 +1,7 @@
+ self::parsePayload($payload),
-                \array_filter(\explode("\n", $data)),
-            ),
-        );
+    public readonly Uuid $uuid;
+    private string $fingerprint;
+    private bool $isExists = false;
+
+    /**
+     * @param PayloadChunkInterface[] $chunks
+     */
+    public function __construct(
+        public readonly array $chunks,
+    ) {
+        $this->uuid = Uuid::generate();
     }
 
-    private static function parsePayload(string $payload): PayloadChunkInterface
+    public function withFingerprint(string $fingerprint): self
     {
-        if (\json_validate($payload)) {
-            $json = \json_decode($payload, true, 512, \JSON_THROW_ON_ERROR);
+        $self = clone $this;
+        $self->fingerprint = $fingerprint;
+        return $self;
+    }
 
-            if (isset($json['type'])) {
-                return new TypeChunk($json);
-            }
+    public function markAsExists(): self
+    {
+        $self = clone $this;
+        $self->isExists = true;
 
-            return new JsonChunk($json);
-        }
+        return $self;
+    }
 
-        return new BlobChunk($payload);
+    public function eventId(): string
+    {
+        return $this->getMeta()->eventId();
     }
 
-    /**
-     * @param PayloadChunkInterface[] $chunks
-     */
-    public function __construct(
-        public array $chunks,
-    ) {}
+    public function traceId(): string
+    {
+        return $this->getMeta()->traceId();
+    }
 
-    public function getMeta(): PayloadChunkInterface
+    public function getMeta(): MetaChunk
     {
-        return $this->chunks[0];
+        if (isset($this->chunks[0]) && $this->chunks[0] instanceof MetaChunk) {
+            return $this->chunks[0];
+        }
+
+        throw new \InvalidArgumentException('Meta chunk not found');
     }
 
     public function getPayload(): PayloadChunkInterface
@@ -59,8 +71,34 @@ public function type(): Type
         throw new \InvalidArgumentException('Type chunk not found');
     }
 
+    public function tags(): array
+    {
+        $serverName = $this->getPayload()['server_name'] ?? null;
+
+        $tags = [
+            'platform' => $this->getMeta()->platform()->name,
+            'environment' => $this->getMeta()->environment(),
+        ];
+
+        if ($serverName !== null) {
+            $tags['server_name'] = $serverName;
+        }
+
+        return $tags;
+    }
+
     public function jsonSerialize(): array
     {
         return $this->chunks;
     }
+
+    public function getFingerprint(): string
+    {
+        return $this->fingerprint;
+    }
+
+    public function isExists(): bool
+    {
+        return $this->isExists;
+    }
 }
diff --git a/app/modules/Sentry/Application/DTO/PayloadChunkInterface.php b/app/modules/Sentry/Application/DTO/PayloadChunkInterface.php
index b236a7ab..402edba1 100644
--- a/app/modules/Sentry/Application/DTO/PayloadChunkInterface.php
+++ b/app/modules/Sentry/Application/DTO/PayloadChunkInterface.php
@@ -4,4 +4,7 @@
 
 namespace Modules\Sentry\Application\DTO;
 
-interface PayloadChunkInterface extends \JsonSerializable, \Stringable {}
+interface PayloadChunkInterface extends \JsonSerializable, \Stringable
+{
+
+}
diff --git a/app/modules/Sentry/Application/DTO/PayloadFactory.php b/app/modules/Sentry/Application/DTO/PayloadFactory.php
new file mode 100644
index 00000000..4a66f18e
--- /dev/null
+++ b/app/modules/Sentry/Application/DTO/PayloadFactory.php
@@ -0,0 +1,66 @@
+ $chunk) {
+            $chunks[] = self::parsePayload($chunk, $i);
+        }
+
+        $platform = self::detectPlatform($chunks);
+
+        return match ($platform) {
+            Platform::React,
+            Platform::Angular,
+            Platform::Javascript => new JavascriptPayload($chunks),
+
+            Platform::VueJs => new VueJsPayload($chunks),
+
+            Platform::PHP,
+            Platform::Laravel,
+            Platform::Symfony => new PHPPayload($chunks),
+
+            default => new Payload($chunks),
+        };
+    }
+
+    private static function parsePayload(string $payload, int $index): PayloadChunkInterface
+    {
+        if (\json_validate($payload)) {
+            $json = \json_decode($payload, true, 512, \JSON_THROW_ON_ERROR);
+
+            if ($index === 0) {
+                return new MetaChunk($json);
+            }
+
+            if (isset($json['type'])) {
+                return new TypeChunk($json);
+            }
+
+            return new JsonChunk($json);
+        }
+
+        return new BlobChunk($payload);
+    }
+
+    /**
+     * @param PayloadChunkInterface[] $chunks
+     */
+    private static function detectPlatform(array $chunks): Platform
+    {
+        foreach ($chunks as $chunk) {
+            if ($chunk instanceof MetaChunk) {
+                return $chunk->platform();
+            }
+        }
+
+        throw new \InvalidArgumentException('Meta chunk not found');
+    }
+}
diff --git a/app/modules/Sentry/Application/DTO/Platform.php b/app/modules/Sentry/Application/DTO/Platform.php
new file mode 100644
index 00000000..32bb729c
--- /dev/null
+++ b/app/modules/Sentry/Application/DTO/Platform.php
@@ -0,0 +1,41 @@
+ self::Python,
+            \str_contains($name, 'ruby') => self::Ruby,
+            \str_contains($name, 'laravel') => self::Laravel,
+            \str_contains($name, 'symfony') => self::Symfony,
+            \str_contains($name, 'php') => self::PHP,
+            \str_contains($name, 'vue') => self::VueJs,
+            \str_contains($name, 'react') => self::React,
+            \str_contains($name, 'angular') => self::React,
+            \str_contains($name, 'javascript') => self::Javascript,
+            default => self::Unknown,
+        };
+    }
+}
diff --git a/app/modules/Sentry/Application/DTO/Type.php b/app/modules/Sentry/Application/DTO/Type.php
index 1230eaff..775510ff 100644
--- a/app/modules/Sentry/Application/DTO/Type.php
+++ b/app/modules/Sentry/Application/DTO/Type.php
@@ -7,6 +7,7 @@
 enum Type
 {
     case Event;
+    case CheckIn;
     case Transaction;
     case ReplyEvent;
     case ReplayRecording;
diff --git a/app/modules/Sentry/Application/DTO/VueJsPayload.php b/app/modules/Sentry/Application/DTO/VueJsPayload.php
new file mode 100644
index 00000000..13cdc3a4
--- /dev/null
+++ b/app/modules/Sentry/Application/DTO/VueJsPayload.php
@@ -0,0 +1,7 @@
+type() === Type::Event) {
+            // TODO: map event to preview
+//            $data = $this->mapper->toPreview(
+//                type: $event->type,
+//                payload: [
+//                    ...$payload->getPayload()->jsonSerialize(),
+//                    'fingerprint' => $payload->getFingerprint(),
+//                    'tags' => $payload->tags(),
+//                ],
+//            );
+
+            $fingerprint = $payload->getFingerprint();
+            $firstEvent = null;
+            $lastEvent = null;
+            $totalEvents = $this->fingerprints->totalEvents($fingerprint);
+
+            if ($totalEvents === 1) {
+                $firstEvent = $lastEvent = $this->fingerprints->findFirstSeen($fingerprint);
+            } elseif ($totalEvents > 1) {
+                $firstEvent = $this->fingerprints->findFirstSeen($fingerprint);
+                $lastEvent = $this->fingerprints->findLastSeen($fingerprint);
+            }
+
+            $this->commands->dispatch(
+                new HandleReceivedEvent(
+                    type: $event->type,
+                    payload: [
+                        'tags' => $payload->tags(),
+                        'total_events' => $totalEvents,
+                        'first_event' => $firstEvent?->getCreatedAt(),
+                        'last_event' => $lastEvent?->getCreatedAt(),
+                        'fingerprint' => $fingerprint,
+                        ...$payload->getPayload()->jsonSerialize(),
+                    ],
+                    project: $event->project,
+//                    uuid: Uuid::fromString($this->md5ToUuid($payload->getFingerprint())),
+                    groupId: $fingerprint,
+                    stackStrategy: StackStrategy::OnlyLatest,
+                ),
+            );
+        }
+
+        return $payload;
+    }
+
+    private function md5ToUuid(string $hash): string
+    {
+        // Inserting hyphens to create a UUID format: 8-4-4-4-12
+        return \substr($hash, 0, 8) . '-' .
+            \substr($hash, 8, 4) . '-' .
+            \substr($hash, 12, 4) . '-' .
+            \substr($hash, 16, 4) . '-' .
+            \substr($hash, 20, 12);
+    }
+}
diff --git a/app/modules/Sentry/Application/Handlers/StoreTraceHandler.php b/app/modules/Sentry/Application/Handlers/StoreTraceHandler.php
new file mode 100644
index 00000000..04bcbe2b
--- /dev/null
+++ b/app/modules/Sentry/Application/Handlers/StoreTraceHandler.php
@@ -0,0 +1,105 @@
+findOrCreateTrace($payload);
+
+        return match ($payload->type()) {
+            Type::Event => $this->storeEvent($payload, $trace),
+            Type::Transaction => $this->storeTransaction($payload, $trace),
+            default => $payload,
+        };
+    }
+
+    private function findOrCreateTrace(Payload $payload): Trace
+    {
+        $trace = $this->traces->findOne(['trace_id' => $payload->traceId()]);
+        if (!$trace) {
+            $trace = $this->traceFactory->createFromMeta(
+                uuid: $payload->uuid,
+                meta: $payload->getMeta(),
+            );
+            $this->em->persist($trace)->run();
+        }
+
+        return $trace;
+    }
+
+    private function storeEvent(Payload $payload, Trace $trace): Payload
+    {
+        $json = $payload->getPayload();
+        \assert($json instanceof JsonChunk);
+
+        $issue = $this->issueFactory->createFromPayload(
+            traceUuid: $trace->getUuid(),
+            payload: $json,
+        );
+
+        $this->em->persist($issue);
+
+        $exceptions = \array_map(
+            static fn(array $exception) => new Exception($exception),
+            (array) ($json['exception']['values'] ?? []),
+        );
+
+        $fingerprint = $this->fingerprintFactory->create(
+            issueUuid: $issue->getUuid(),
+            exceptions: $exceptions,
+        );
+
+        $this->em->persist($fingerprint);
+        $this->storeTags($payload, $issue);
+        $this->em->run();
+
+        return $payload->withFingerprint($fingerprint->getFingerprint());
+    }
+
+    private function storeTransaction(Payload $payload, Trace $trace): Payload
+    {
+        // todo: implement
+
+        return $payload;
+    }
+
+    private function storeTags(Payload $payload, Issue $issue): void
+    {
+        foreach ($payload->tags() as $tag => $value) {
+            $issue->getTags()->add(
+                new IssueTag(
+                    issueUuid: $issue->getUuid(),
+                    tag: $tag,
+                    value: (string) $value,
+                ),
+            );
+        }
+    }
+}
diff --git a/app/modules/Sentry/Application/Mapper/EventTypeMapper.php b/app/modules/Sentry/Application/Mapper/EventTypeMapper.php
index cb75f04b..8f05c5b9 100644
--- a/app/modules/Sentry/Application/Mapper/EventTypeMapper.php
+++ b/app/modules/Sentry/Application/Mapper/EventTypeMapper.php
@@ -22,6 +22,8 @@ public function toPreview(string $type, array|\JsonSerializable $payload): array
                 exception: $data['exception'] ?? null,
                 max: $this->maxExceptions,
             ),
+            'tags' => $data['tags'] ?? [],
+            'fingerprint' => $data['fingerprint'] ?? null,
             'level' => $data['level'] ?? null,
             'platform' => $data['platform'] ?? null,
             'environment' => $data['environment'] ?? null,
diff --git a/app/modules/Sentry/Application/PayloadParser.php b/app/modules/Sentry/Application/PayloadParser.php
index f0fb8d5a..eac8d9a8 100644
--- a/app/modules/Sentry/Application/PayloadParser.php
+++ b/app/modules/Sentry/Application/PayloadParser.php
@@ -5,10 +5,9 @@
 namespace Modules\Sentry\Application;
 
 use App\Application\HTTP\GzippedStreamFactory;
-use Modules\Sentry\Application\DTO\BlobChunk;
 use Modules\Sentry\Application\DTO\JsonChunk;
 use Modules\Sentry\Application\DTO\Payload;
-use Modules\Sentry\Application\DTO\TypeChunk;
+use Modules\Sentry\Application\DTO\PayloadFactory;
 use Psr\Http\Message\ServerRequestInterface;
 
 final readonly class PayloadParser
@@ -22,32 +21,16 @@ public function parse(ServerRequestInterface $request): Payload
         $isV4 = $request->getHeaderLine('Content-Type') === 'application/x-sentry-envelope' ||
             \str_contains($request->getHeaderLine('X-Sentry-Auth'), 'sentry_client=sentry.php');
 
-        if ($isV4) {
-            if ($request->getHeaderLine('Content-Encoding') === 'gzip') {
-                $chunks = [];
-
-                foreach ($this->gzippedStreamFactory->createFromRequest($request)->getPayload() as $payload) {
-                    if (\is_string($payload)) {
-                        $chunks[] = new BlobChunk($payload);
-                        continue;
-                    }
-
-                    if (isset($payload['type'])) {
-                        $chunks[] = new TypeChunk($payload);
-                        continue;
-                    }
-
-                    $chunks[] = new JsonChunk($payload);
-                }
-
-                return new Payload($chunks);
-            }
+        if (!$isV4) {
+            throw new \InvalidArgumentException('Unsupported Sentry protocol version');
+        }
 
-            return Payload::parse((string) $request->getBody());
+        if ($request->getHeaderLine('Content-Encoding') === 'gzip') {
+            return PayloadFactory::parseJson(
+                $this->gzippedStreamFactory->createFromRequest($request)->getPayload(),
+            );
         }
 
-        return new Payload(
-            [new JsonChunk($request->getParsedBody())],
-        );
+        return PayloadFactory::parseJson((string) $request->getBody());
     }
 }
diff --git a/app/modules/Sentry/Application/SentryBootloader.php b/app/modules/Sentry/Application/SentryBootloader.php
index 19a35a61..1bf4e2e7 100644
--- a/app/modules/Sentry/Application/SentryBootloader.php
+++ b/app/modules/Sentry/Application/SentryBootloader.php
@@ -5,7 +5,30 @@
 namespace Modules\Sentry\Application;
 
 use App\Application\Event\EventTypeRegistryInterface;
+use Cycle\Database\DatabaseInterface;
+use Cycle\ORM\ORMInterface;
+use Cycle\ORM\Select;
+use Modules\Sentry\Application\Handlers\StoreEventHandler;
+use Modules\Sentry\Application\Handlers\StoreTraceHandler;
+use Modules\Sentry\Domain\Fingerprint;
+use Modules\Sentry\Domain\FingerprintFactoryInterface;
+use Modules\Sentry\Domain\FingerprintRepositoryInterface;
+use Modules\Sentry\Domain\Issue;
+use Modules\Sentry\Domain\IssueFactoryInterface;
+use Modules\Sentry\Domain\IssueRepositoryInterface;
+use Modules\Sentry\Domain\IssueTag;
+use Modules\Sentry\Domain\IssueTagRepositoryInterface;
+use Modules\Sentry\Domain\Trace;
+use Modules\Sentry\Domain\TraceFactoryInterface;
+use Modules\Sentry\Domain\TraceRepositoryInterface;
 use Modules\Sentry\EventHandler;
+use Modules\Sentry\Integration\CycleOrm\FingerprintFactory;
+use Modules\Sentry\Integration\CycleOrm\FingerprintRepository;
+use Modules\Sentry\Integration\CycleOrm\IssueFactory;
+use Modules\Sentry\Integration\CycleOrm\IssueRepository;
+use Modules\Sentry\Integration\CycleOrm\IssueTagRepository;
+use Modules\Sentry\Integration\CycleOrm\TraceFactory;
+use Modules\Sentry\Integration\CycleOrm\TraceRepository;
 use Psr\Container\ContainerInterface;
 use Spiral\Boot\Bootloader\Bootloader;
 use Spiral\Boot\EnvironmentInterface;
@@ -23,7 +46,32 @@ public function defineSingletons(): array
 
             EventHandlerInterface::class => static fn(
                 ContainerInterface $container,
-            ): EventHandlerInterface => new EventHandler($container, []),
+            ): EventHandlerInterface => new EventHandler($container, [
+                StoreTraceHandler::class,
+                StoreEventHandler::class,
+            ]),
+
+            // Persistence
+            IssueTagRepositoryInterface::class => static fn(
+                ORMInterface $orm,
+            ): IssueTagRepositoryInterface => new IssueTagRepository(new Select($orm, IssueTag::class)),
+            FingerprintRepositoryInterface::class => static fn(
+                ORMInterface $orm,
+                DatabaseInterface $database,
+            ): FingerprintRepositoryInterface => new FingerprintRepository(
+                new Select($orm, Fingerprint::class),
+                $database,
+            ),
+            IssueRepositoryInterface::class => static fn(
+                ORMInterface $orm,
+            ): IssueRepositoryInterface => new IssueRepository(new Select($orm, Issue::class)),
+            TraceRepositoryInterface::class => static fn(
+                ORMInterface $orm,
+            ): TraceRepositoryInterface => new TraceRepository(new Select($orm, Trace::class)),
+
+            TraceFactoryInterface::class => TraceFactory::class,
+            IssueFactoryInterface::class => IssueFactory::class,
+            FingerprintFactoryInterface::class => FingerprintFactory::class,
         ];
     }
 
diff --git a/app/modules/Sentry/Domain/Fingerprint.php b/app/modules/Sentry/Domain/Fingerprint.php
new file mode 100644
index 00000000..38c51d85
--- /dev/null
+++ b/app/modules/Sentry/Domain/Fingerprint.php
@@ -0,0 +1,55 @@
+createdAt = new \DateTimeImmutable();
+    }
+
+    public function getIssueUuid(): Uuid
+    {
+        return $this->issueUuid;
+    }
+
+    public function getFingerprint(): string
+    {
+        return $this->fingerprint;
+    }
+
+    public function getCreatedAt(): \DateTimeInterface
+    {
+        return $this->createdAt;
+    }
+}
diff --git a/app/modules/Sentry/Domain/FingerprintFactoryInterface.php b/app/modules/Sentry/Domain/FingerprintFactoryInterface.php
new file mode 100644
index 00000000..df94c6b3
--- /dev/null
+++ b/app/modules/Sentry/Domain/FingerprintFactoryInterface.php
@@ -0,0 +1,16 @@
+tags = new ArrayCollection();
+        $this->createdAt = new \DateTimeImmutable();
+    }
+
+    public function getUuid(): Uuid
+    {
+        return $this->uuid;
+    }
+
+    public function getTraceUuid(): Uuid
+    {
+        return $this->traceUuid;
+    }
+
+    public function getTitle(): string
+    {
+        return $this->title;
+    }
+
+    public function getPlatform(): string
+    {
+        return $this->platform;
+    }
+
+    public function getLogger(): string
+    {
+        return $this->logger;
+    }
+
+    public function getType(): string
+    {
+        return $this->type;
+    }
+
+    public function getSdk(): Sdk
+    {
+        return $this->sdk;
+    }
+
+    public function getTransaction(): string
+    {
+        return $this->transaction;
+    }
+
+    public function getServerName(): string
+    {
+        return $this->serverName;
+    }
+
+    public function getPayload(): Json
+    {
+        return $this->payload;
+    }
+
+    public function getCreatedAt(): \DateTimeInterface
+    {
+        return $this->createdAt;
+    }
+
+    public function getTags(): ArrayCollection
+    {
+        return $this->tags;
+    }
+}
diff --git a/app/modules/Sentry/Domain/IssueFactoryInterface.php b/app/modules/Sentry/Domain/IssueFactoryInterface.php
new file mode 100644
index 00000000..4f70d21f
--- /dev/null
+++ b/app/modules/Sentry/Domain/IssueFactoryInterface.php
@@ -0,0 +1,16 @@
+issueUuid;
+    }
+
+    public function getTag(): string
+    {
+        return $this->tag;
+    }
+
+    public function getValue(): string
+    {
+        return $this->value;
+    }
+}
diff --git a/app/modules/Sentry/Domain/IssueTagRepositoryInterface.php b/app/modules/Sentry/Domain/IssueTagRepositoryInterface.php
new file mode 100644
index 00000000..b32d9a94
--- /dev/null
+++ b/app/modules/Sentry/Domain/IssueTagRepositoryInterface.php
@@ -0,0 +1,12 @@
+uuid;
+    }
+
+    public function getTraceId(): string
+    {
+        return $this->traceId;
+    }
+
+    public function getPublicKey(): string
+    {
+        return $this->publicKey;
+    }
+
+    public function getEnvironment(): string
+    {
+        return $this->environment;
+    }
+
+    public function isSampled(): bool
+    {
+        return $this->sampled;
+    }
+
+    public function getSampleRate(): float
+    {
+        return $this->sampleRate;
+    }
+
+    public function getTransaction(): string
+    {
+        return $this->transaction;
+    }
+
+    public function getSdk(): Json
+    {
+        return $this->sdk;
+    }
+
+    public function getLanguage(): string
+    {
+        return $this->language;
+    }
+}
diff --git a/app/modules/Sentry/Domain/TraceFactoryInterface.php b/app/modules/Sentry/Domain/TraceFactoryInterface.php
new file mode 100644
index 00000000..f502c30d
--- /dev/null
+++ b/app/modules/Sentry/Domain/TraceFactoryInterface.php
@@ -0,0 +1,16 @@
+handlers as $handler) {
-            $event = $this->container->get($handler)->handle($event);
+            $payload = $this->container->get($handler)->handle($payload, $event);
         }
 
-        return $event;
+        return $payload;
     }
 }
diff --git a/app/modules/Sentry/Integration/CycleOrm/FingerprintFactory.php b/app/modules/Sentry/Integration/CycleOrm/FingerprintFactory.php
new file mode 100644
index 00000000..bd787ba4
--- /dev/null
+++ b/app/modules/Sentry/Integration/CycleOrm/FingerprintFactory.php
@@ -0,0 +1,28 @@
+calculateFingerprint();
+        }
+
+        return new Fingerprint(
+            uuid: Uuid::generate(),
+            issueUuid: $issueUuid,
+            fingerprint: \md5(\implode('', $fingerprints)),
+        );
+    }
+}
diff --git a/app/modules/Sentry/Integration/CycleOrm/FingerprintRepository.php b/app/modules/Sentry/Integration/CycleOrm/FingerprintRepository.php
new file mode 100644
index 00000000..ede94eae
--- /dev/null
+++ b/app/modules/Sentry/Integration/CycleOrm/FingerprintRepository.php
@@ -0,0 +1,78 @@
+select()
+            ->where('fingerprint', $fingerprint)
+            ->orderBy('created_at', 'ASC')
+            ->fetchOne();
+    }
+
+    public function findLastSeen(string $fingerprint): ?Fingerprint
+    {
+        return $this->select()
+            ->where('fingerprint', $fingerprint)
+            ->orderBy('created_at', 'DESC')
+            ->fetchOne();
+    }
+
+    public function totalEvents(string $fingerprint): int
+    {
+        return $this->select()
+            ->where('fingerprint', $fingerprint)
+            ->count();
+    }
+
+    public function stat(string $fingerprint, int $days = 7): array
+    {
+        $rage = Carbon::now()->subDays($days)->toPeriod(Carbon::now(), CarbonInterval::day());
+
+        $result = $this->database->select([
+            new Fragment('DATE(created_at) as date'),
+            new Fragment('COUNT(*) as count'),
+        ])
+            ->from('sentry_issue_fingerprints')
+            ->where('created_at', '>=', $rage->getStartDate())
+            ->where('fingerprint', $fingerprint)
+            ->groupBy('date')
+            ->fetchAll();
+
+        $result = array_combine(
+            array_column($result, 'date'),
+            array_column($result, 'count'),
+        );
+
+        $stat = [];
+        foreach ($rage as $date) {
+            $stat[$date->format('Y-m-d')] = [
+                'date' => $date->format('Y-m-d'),
+                'count' => $result[$date->format('Y-m-d')] ?? 0,
+            ];
+        }
+
+        return \array_values($stat);
+    }
+}
diff --git a/app/modules/Sentry/Integration/CycleOrm/IssueFactory.php b/app/modules/Sentry/Integration/CycleOrm/IssueFactory.php
new file mode 100644
index 00000000..c6947c76
--- /dev/null
+++ b/app/modules/Sentry/Integration/CycleOrm/IssueFactory.php
@@ -0,0 +1,45 @@
+generateTitle($payload),
+            platform: $payload['platform'] ?? 'unknown',
+            logger: $payload['logger'] ?? 'unknown',
+            type: 'error',
+            transaction: $payload['transaction'] ?? null,
+            serverName: $payload['server_name'] ?? '',
+            payload: new Json($payload),
+        );
+    }
+
+    private function generateTitle(JsonChunk $payload): string
+    {
+        $title = 'Unknown error';
+        $exceptions = \array_reverse((array) ($payload['exception']['values'] ?? []));
+
+        foreach ($exceptions as $exception) {
+            if (isset($exception['value'])) {
+                return $exception['value'];
+            }
+        }
+
+        return $title;
+    }
+}
diff --git a/app/modules/Sentry/Integration/CycleOrm/IssueRepository.php b/app/modules/Sentry/Integration/CycleOrm/IssueRepository.php
new file mode 100644
index 00000000..eba55957
--- /dev/null
+++ b/app/modules/Sentry/Integration/CycleOrm/IssueRepository.php
@@ -0,0 +1,21 @@
+select()
+            ->where('fingerprint.fingerprint', $fingerprint)
+            ->orderBy('created_at', 'DESC')
+            ->fetchOne();
+    }
+}
diff --git a/app/modules/Sentry/Integration/CycleOrm/IssueTagRepository.php b/app/modules/Sentry/Integration/CycleOrm/IssueTagRepository.php
new file mode 100644
index 00000000..64b89ba1
--- /dev/null
+++ b/app/modules/Sentry/Integration/CycleOrm/IssueTagRepository.php
@@ -0,0 +1,13 @@
+traceId(),
+            publicKey: $meta->publicKey(),
+            environment: $meta->environment(),
+            sampled: $meta->sampled(),
+            sampleRate: $meta->sampleRate(),
+            transaction: $meta->transaction(),
+            sdk: new Sdk($meta->sdk()),
+            language: $meta->platform(),
+        );
+    }
+}
diff --git a/app/modules/Sentry/Integration/CycleOrm/TraceRepository.php b/app/modules/Sentry/Integration/CycleOrm/TraceRepository.php
new file mode 100644
index 00000000..428f4a62
--- /dev/null
+++ b/app/modules/Sentry/Integration/CycleOrm/TraceRepository.php
@@ -0,0 +1,13 @@
+getUri()->getPath(), '/');
         $project = \explode('/', $url)[2] ?? null;
 
-        $event = new EventType(type: 'sentry', project: $project);
-
         $payload = $this->payloadParser->parse($request);
-
-        match (true) {
-            \str_ends_with($url, '/envelope') => $this->handleEnvelope($payload, $event),
-            \str_ends_with($url, '/store') => $this->handleEvent($payload->getMeta(), $event),
-            default => null,
-        };
+        $this->handler->handle($payload, new EventType(type: 'sentry', project: $project));
 
         return $this->responseWrapper->create(200);
     }
-
-    private function handleEvent(PayloadChunkInterface $chunk, EventType $eventType): void
-    {
-        $event = $this->handler->handle($chunk->jsonSerialize());
-
-        $this->commands->dispatch(
-            new HandleReceivedEvent(
-                type: $eventType->type,
-                payload: $event,
-                project: $eventType->project,
-            ),
-        );
-    }
-
-    /**
-     * TODO handle sentry transaction and session
-     */
-    private function handleEnvelope(Payload $data, EventType $eventType): void
-    {
-        match ($data->type()) {
-            Type::Event => $this->handleEvent($data->getPayload(), $eventType),
-            default => null,
-        };
-    }
 }
diff --git a/app/modules/Sentry/Interfaces/Http/Handler/JsEventHandler.php b/app/modules/Sentry/Interfaces/Http/Handler/JsEventHandler.php
index 5f01a6b1..8d5b813a 100644
--- a/app/modules/Sentry/Interfaces/Http/Handler/JsEventHandler.php
+++ b/app/modules/Sentry/Interfaces/Http/Handler/JsEventHandler.php
@@ -4,16 +4,13 @@
 
 namespace Modules\Sentry\Interfaces\Http\Handler;
 
-use App\Application\Commands\HandleReceivedEvent;
 use App\Application\Event\EventType;
 use App\Application\Service\HttpHandler\HandlerInterface;
-use Modules\Sentry\Application\DTO\Payload;
-use Modules\Sentry\Application\DTO\Type;
+use Modules\Sentry\Application\DTO\PayloadFactory;
 use Modules\Sentry\Application\EventHandlerInterface;
 use Modules\Sentry\Application\SecretKeyValidator;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
-use Spiral\Cqrs\CommandBusInterface;
 use Spiral\Http\Exception\ClientException\ForbiddenException;
 use Spiral\Http\ResponseWrapper;
 
@@ -22,7 +19,6 @@
     public function __construct(
         private ResponseWrapper $responseWrapper,
         private EventHandlerInterface $handler,
-        private CommandBusInterface $commands,
         private SecretKeyValidator $secretKeyValidator,
     ) {}
 
@@ -45,36 +41,13 @@ public function handle(ServerRequestInterface $request, \Closure $next): Respons
         $url = \rtrim($request->getUri()->getPath(), '/');
         $project = \explode('/', $url)[2] ?? null;
 
-        $event = new EventType(type: 'sentry', project: $project);
-        $payload = Payload::parse((string) $request->getBody());
+        $payload = PayloadFactory::parseJson((string) $request->getBody());
 
-        match ($payload->type()) {
-            Type::Event => $this->handleEvent($payload, $event),
-            // TODO handle sentry transaction and session
-            // Type::Transaction => ...,
-            // TODO handle sentry reply recordings
-            // Type::ReplayRecording => ...,
-            default => null,
-        };
+        $this->handler->handle($payload, new EventType(type: 'sentry', project: $project));
 
         return $this->responseWrapper->create(200);
     }
 
-    private function handleEvent(Payload $payload, EventType $eventType): void
-    {
-        $event = $this->handler->handle(
-            $payload->getPayload()->jsonSerialize(),
-        );
-
-        $this->commands->dispatch(
-            new HandleReceivedEvent(
-                type: $eventType->type,
-                payload: $event,
-                project: $eventType->project,
-            ),
-        );
-    }
-
     private function isValidRequest(ServerRequestInterface $request): bool
     {
         return isset($request->getQueryParams()['sentry_key']);
diff --git a/app/modules/Sentry/Interfaces/Http/ShowIssueAction.php b/app/modules/Sentry/Interfaces/Http/ShowIssueAction.php
new file mode 100644
index 00000000..eae34f58
--- /dev/null
+++ b/app/modules/Sentry/Interfaces/Http/ShowIssueAction.php
@@ -0,0 +1,38 @@
+', name: 'sentry.latest_issue', methods: 'GET', group: 'api')]
+    public function __invoke(string $fingerprint): ResourceInterface
+    {
+        $issue = $this->issues->findLatestByFingerprint($fingerprint);
+
+        if (!$issue) {
+            throw new EntityNotFoundException('Issue not found');
+        }
+
+        return new JsonResource([
+            'uuid' => (string) $issue->getUuid(),
+            'title' => $issue->getTitle(),
+            'platform' => $issue->getPlatform(),
+            'logger' => $issue->getLogger(),
+            'type' => $issue->getType(),
+            'transaction' => $issue->getTransaction(),
+            ...$issue->getPayload()->jsonSerialize(),
+        ]);
+    }
+}
diff --git a/app/modules/Sentry/Interfaces/Http/ShowIssueStatAction.php b/app/modules/Sentry/Interfaces/Http/ShowIssueStatAction.php
new file mode 100644
index 00000000..e86574a3
--- /dev/null
+++ b/app/modules/Sentry/Interfaces/Http/ShowIssueStatAction.php
@@ -0,0 +1,42 @@
+/stat', name: 'sentry.issue.stat', methods: 'GET', group: 'api')]
+    public function __invoke(string $fingerprint): array
+    {
+        $days = 14;
+
+        $stat = $this->fingerprints->stat($fingerprint, $days);
+        $firstEvent = null;
+        $lastEvent = null;
+        $totalEvents = $this->fingerprints->totalEvents($fingerprint);
+
+        if ($totalEvents === 1) {
+            $firstEvent = $lastEvent = $this->fingerprints->findFirstSeen($fingerprint);
+        } elseif ($totalEvents > 1) {
+            $firstEvent = $this->fingerprints->findFirstSeen($fingerprint);
+            $lastEvent = $this->fingerprints->findLastSeen($fingerprint);
+        }
+
+        return [
+            'total_events' => $totalEvents,
+            'first_event' => $firstEvent?->getCreatedAt()?->format(\DateTimeInterface::W3C),
+            'last_event' => $lastEvent?->getCreatedAt()?->format(\DateTimeInterface::W3C),
+            'fingerprint' => $fingerprint,
+            'stat_days' => $days,
+            'stat' => $stat,
+        ];
+    }
+}
diff --git a/app/src/Application/Commands/HandleReceivedEvent.php b/app/src/Application/Commands/HandleReceivedEvent.php
index 636952e5..6a9cd35c 100644
--- a/app/src/Application/Commands/HandleReceivedEvent.php
+++ b/app/src/Application/Commands/HandleReceivedEvent.php
@@ -5,6 +5,7 @@
 namespace App\Application\Commands;
 
 use App\Application\Domain\ValueObjects\Uuid;
+use App\Application\Event\StackStrategy;
 use Spiral\Cqrs\CommandInterface;
 
 final readonly class HandleReceivedEvent implements CommandInterface, \JsonSerializable
@@ -17,6 +18,8 @@ public function __construct(
         public array|\JsonSerializable $payload,
         public ?string $project = null,
         ?Uuid $uuid = null,
+        public ?string $groupId = null,
+        public StackStrategy $stackStrategy = StackStrategy::None,
     ) {
         $this->uuid = $uuid ?? Uuid::generate();
         $this->timestamp = \microtime(true);
@@ -30,6 +33,7 @@ public function jsonSerialize(): array
             'payload' => $this->payload,
             'uuid' => (string) $this->uuid,
             'timestamp' => $this->timestamp,
+            'groupId' => $this->groupId,
         ];
     }
 }
diff --git a/app/src/Application/Domain/ValueObjects/Json.php b/app/src/Application/Domain/ValueObjects/Json.php
index 56fd5982..a123ea9e 100644
--- a/app/src/Application/Domain/ValueObjects/Json.php
+++ b/app/src/Application/Domain/ValueObjects/Json.php
@@ -4,7 +4,7 @@
 
 namespace App\Application\Domain\ValueObjects;
 
-final readonly class Json implements \JsonSerializable, \Stringable
+readonly class Json implements \JsonSerializable, \Stringable
 {
     public function __construct(
         private array|\JsonSerializable $data = [],
@@ -16,11 +16,11 @@ public function __construct(
     final public static function typecast(mixed $value): self
     {
         if (empty($value)) {
-            return new self();
+            return new static();
         }
 
         try {
-            return new self(
+            return new static(
                 (array) \json_decode($value, true),
             );
         } catch (\JsonException $e) {
diff --git a/app/src/Application/Event/StackStrategy.php b/app/src/Application/Event/StackStrategy.php
new file mode 100644
index 00000000..3adffce6
--- /dev/null
+++ b/app/src/Application/Event/StackStrategy.php
@@ -0,0 +1,12 @@
+
+     * @return string
      */
-    public function getPayload(): iterable
+    public function getPayload(): string
     {
-        $payloads = \array_filter(\explode("\n", (string) $this->stream));
-
-        foreach ($payloads as $payload) {
-            if (!\json_validate($payload)) {
-                yield $payload;
-                continue;
-            }
-
-            yield \json_decode($payload, true, 512, \JSON_THROW_ON_ERROR);
-        }
+        return (string) $this->stream;
     }
 }
diff --git a/tests/Feature/Interfaces/Http/Sentry/SentryV3ActionTest.php b/tests/Feature/Interfaces/Http/Sentry/SentryV3ActionTest.php
deleted file mode 100644
index 588516d7..00000000
--- a/tests/Feature/Interfaces/Http/Sentry/SentryV3ActionTest.php
+++ /dev/null
@@ -1,90 +0,0 @@
-run($_SERVER['argv'], $exit);","post_context":["        } catch (Throwable $t) {","            throw new RuntimeException(","                $t->getMessage(),","                (int) $t->getCode(),","                $t,"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","lineno":144,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","function":"PHPUnit\\TextUI\\Command::run","raw_function":"PHPUnit\\TextUI\\Command::run","pre_context":["        }","","        unset($this->arguments['test'], $this->arguments['testFile']);","","        try {"],"context_line":"            $result = $runner->run($suite, $this->arguments, $this->warnings, $exit);","post_context":["        } catch (Throwable $t) {","            print $t->getMessage() . PHP_EOL;","        }","","        $return = TestRunner::FAILURE_EXIT;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","lineno":651,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","function":"PHPUnit\\TextUI\\TestRunner::run","raw_function":"PHPUnit\\TextUI\\TestRunner::run","pre_context":["            if ($extension instanceof BeforeFirstTestHook) {","                $extension->executeBeforeFirstTest();","            }","        }",""],"context_line":"        $suite->run($result);","post_context":["","        foreach ($this->extensions as $extension) {","            if ($extension instanceof AfterLastTestHook) {","                $extension->executeAfterLastTest();","            }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":["                $test->setBackupGlobals($this->backupGlobals);","                $test->setBackupStaticAttributes($this->backupStaticAttributes);","                $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);","            }",""],"context_line":"            $test->run($result);","post_context":["        }","","        if ($this->testCase && class_exists($this->name, false)) {","            foreach ($hookMethods['afterClass'] as $afterClassMethod) {","                if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":["                $test->setBackupGlobals($this->backupGlobals);","                $test->setBackupStaticAttributes($this->backupStaticAttributes);","                $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);","            }",""],"context_line":"            $test->run($result);","post_context":["        }","","        if ($this->testCase && class_exists($this->name, false)) {","            foreach ($hookMethods['afterClass'] as $afterClassMethod) {","                if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":968,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::run","raw_function":"PHPUnit\\Framework\\TestCase::run","pre_context":["            $template->setVar($var);","","            $php = AbstractPhpProcess::factory();","            $php->runTestJob($template->render(), $this, $result, $processResultFile);","        } else {"],"context_line":"            $result->run($this);","post_context":["        }","","        $this->result = null;","","        return $result;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","lineno":728,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","function":"PHPUnit\\Framework\\TestResult::run","raw_function":"PHPUnit\\Framework\\TestResult::run","pre_context":["                        $_timeout = $this->defaultTimeLimit;","                }","","                $invoker->invoke([$test, 'runBare'], [], $_timeout);","            } else {"],"context_line":"                $test->runBare();","post_context":["            }","        } catch (TimeoutException $e) {","            $this->addFailure(","                $test,","                new RiskyTestError("]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1218,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runBare","raw_function":"PHPUnit\\Framework\\TestCase::runBare","pre_context":["","            foreach ($hookMethods['preCondition'] as $method) {","                $this->{$method}();","            }",""],"context_line":"            $this->testResult = $this->runTest();","post_context":["            $this->verifyMockObjects();","","            foreach ($hookMethods['postCondition'] as $method) {","                $this->{$method}();","            }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1612,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runTest","raw_function":"PHPUnit\\Framework\\TestCase::runTest","pre_context":["        $testArguments = array_merge($this->data, $this->dependencyInput);","","        $this->registerMockObjectsFromTestArguments($testArguments);","","        try {"],"context_line":"            $testResult = $this->{$this->name}(...array_values($testArguments));","post_context":["        } catch (Throwable $exception) {","            if (!$this->checkExceptionExpectations($exception)) {","                throw $exception;","            }",""]},{"filename":"\/tests\/Feature\/Interfaces\/Http\/HttpDumps\/HttpDumpsActionTest.php","lineno":14,"in_app":true,"abs_path":"\\/tests\/Feature\/Interfaces\/Http\/HttpDumps\/HttpDumpsActionTest.php","function":"Tests\\Feature\\Interfaces\\Http\\HttpDumps\\HttpDumpsActionTest::testHttpDumpsPost","raw_function":"Tests\\Feature\\Interfaces\\Http\\HttpDumps\\HttpDumpsActionTest::testHttpDumpsPost","pre_context":["final class HttpDumpsActionTest extends ControllerTestCase","{","    public function testHttpDumpsPost(): void","    {","        \\Sentry\\init(['dsn' => 'http:\/\/user@127.0.0.1:8082\/1']);"],"context_line":"        \\Sentry\\captureException(new \\Exception('test'));","post_context":["","","        $this->http","            ->postJson(","                uri: '\/',"]}]},"mechanism":{"type":"generic","handled":true,"data":{"code":0}}}]}}
-BODY;
-
-    private Project $project;
-
-    protected function setUp(): void
-    {
-        parent::setUp();
-
-        $this->project = $this->createProject('default');
-    }
-
-    public function testSend(): void
-    {
-        $this->makeRequest(project: $this->project->getKey());
-        $this->assertEventSent($this->project->getKey());
-    }
-
-    public function testSendWithNonExistsProject(): void
-    {
-        $this->makeRequest(project: 'non-exists');
-        $this->assertEventSent();
-    }
-
-    #[Env('SENTRY_SECRET_KEY', 'secret')]
-    public function testSendWithSecretKeyValidation(): void
-    {
-        $this->makeRequest(secret: 'secret', project: $this->project->getKey());
-        $this->assertEventSent($this->project->getKey());
-    }
-
-    #[Env('SENTRY_SECRET_KEY', 'secret')]
-    public function testSendWithInvalidSecretKey(): void
-    {
-        $this->makeRequest(secret: 'invalid', project: $this->project->getKey())
-            ->assertForbidden();
-
-        $this->broadcastig->assertNotPushed(new EventsChannel($this->project->getKey()));
-    }
-
-    public function assertEventSent(Key|string|null $project = null): void
-    {
-        $this->broadcastig->assertPushed(new EventsChannel($project), function (array $data) use ($project) {
-            $this->assertSame('event.received', $data['event']);
-            $this->assertSame('sentry', $data['data']['type']);
-            $this->assertSame($project ? (string) $project : null, $data['data']['project']);
-
-            $this->assertSame('f7b7f09d40e645c79a8a2846e2111c81', $data['data']['payload']['event_id']);
-            $this->assertSame('php', $data['data']['payload']['platform']);
-            $this->assertSame('Test', $data['data']['payload']['server_name']);
-            $this->assertSame('production', $data['data']['payload']['environment']);
-
-            $this->assertNotEmpty($data['data']['uuid']);
-            $this->assertNotEmpty($data['data']['timestamp']);
-
-
-            return true;
-        });
-    }
-
-    private function makeRequest(string $secret = 'secret', string|Key $project = 'default'): ResponseAssertions
-    {
-        return $this->http
-            ->postJson(
-                uri: '/api/' . $project . '/store/',
-                data: Stream::create(self::PAYLOAD),
-                headers: [
-                    'X-Sentry-Auth' => 'Sentry sentry_version=7, sentry_client=sentry.php/4.0.1, sentry_key=' . $secret,
-                ],
-            );
-    }
-}
diff --git a/tests/Feature/Interfaces/Http/Sentry/SentryV4ActionTest.php b/tests/Feature/Interfaces/Http/Sentry/SentryV4ActionTest.php
index b94a2ae8..00973391 100644
--- a/tests/Feature/Interfaces/Http/Sentry/SentryV4ActionTest.php
+++ b/tests/Feature/Interfaces/Http/Sentry/SentryV4ActionTest.php
@@ -16,7 +16,7 @@ final class SentryV4ActionTest extends ControllerTestCase
     protected const JSON = <<<'BODY'
 {"event_id":"2b4f7918973f4371933dce5b3ac381bd","sent_at":"2023-12-01T18:30:35Z","dsn":"http:\/\/user@127.0.0.1:8082\/1","sdk":{"name":"sentry.php","version":"4.0.1"},"trace":{"trace_id":"143ef743ce184eb7abd0ae0891d33b7d","public_key":"user"}}
 {"type":"event","content_type":"application\/json"}
-{"timestamp":1701455435.634665,"platform":"php","sdk":{"name":"sentry.php","version":"4.0.1"},"server_name":"Test","environment":"production","modules":{"amphp\/amp":"v2.6.2","amphp\/byte-stream":"v1.8.1","brick\/math":"0.11.0","buggregator\/app":"dev-master@818ea82","clue\/stream-filter":"v1.6.0","cocur\/slugify":"v3.2","codedungeon\/php-cli-colors":"1.12.2","composer\/pcre":"3.1.1","composer\/semver":"3.4.0","composer\/xdebug-handler":"3.0.3","cycle\/annotated":"v3.4.0","cycle\/database":"2.6.1","cycle\/migrations":"v4.2.1","cycle\/orm":"v2.5.0","cycle\/schema-builder":"v2.6.1","cycle\/schema-migrations-generator":"2.2.0","cycle\/schema-renderer":"1.2.0","defuse\/php-encryption":"v2.4.0","dnoegel\/php-xdg-base-dir":"v0.1.1","doctrine\/annotations":"2.0.1","doctrine\/collections":"1.8.0","doctrine\/deprecations":"1.1.2","doctrine\/inflector":"2.0.8","doctrine\/instantiator":"2.0.0","doctrine\/lexer":"3.0.0","egulias\/email-validator":"4.0.2","felixfbecker\/advanced-json-rpc":"v3.2.1","felixfbecker\/language-server-protocol":"v1.5.2","fidry\/cpu-core-counter":"0.5.1","google\/common-protos":"v4.5.0","google\/protobuf":"v3.25.1","graham-campbell\/result-type":"v1.1.2","grpc\/grpc":"1.57.0","guzzlehttp\/psr7":"2.6.1","hamcrest\/hamcrest-php":"v2.0.1","jean85\/pretty-package-versions":"2.0.5","league\/event":"3.0.2","league\/flysystem":"2.5.0","league\/mime-type-detection":"1.14.0","mockery\/mockery":"1.6.6","monolog\/monolog":"2.9.2","myclabs\/deep-copy":"1.11.1","nesbot\/carbon":"2.71.0","netresearch\/jsonmapper":"v4.2.0","nette\/php-generator":"v4.1.2","nette\/utils":"v4.0.3","nikic\/php-parser":"v4.17.1","nyholm\/psr7":"1.8.1","paragonie\/random_compat":"v9.99.100","phar-io\/manifest":"2.0.3","phar-io\/version":"3.2.1","php-http\/message":"1.16.0","phpdocumentor\/reflection-common":"2.2.0","phpdocumentor\/reflection-docblock":"5.3.0","phpdocumentor\/type-resolver":"1.7.3","phpoption\/phpoption":"1.9.2","phpstan\/phpdoc-parser":"1.24.4","phpunit\/php-code-coverage":"9.2.29","phpunit\/php-file-iterator":"3.0.6","phpunit\/php-invoker":"3.1.1","phpunit\/php-text-template":"2.0.4","phpunit\/php-timer":"5.0.3","phpunit\/phpunit":"9.6.15","pimple\/pimple":"v3.5.0","psr\/cache":"3.0.0","psr\/clock":"1.0.0","psr\/container":"2.0.2","psr\/event-dispatcher":"1.0.0","psr\/http-factory":"1.0.2","psr\/http-message":"2.0","psr\/http-server-handler":"1.0.2","psr\/http-server-middleware":"1.0.2","psr\/log":"3.0.0","psr\/simple-cache":"3.0.0","qossmic\/deptrac-shim":"1.0.2","ralouphie\/getallheaders":"3.0.3","ramsey\/collection":"2.0.0","ramsey\/uuid":"4.7.5","roadrunner-php\/app-logger":"1.1.0","roadrunner-php\/centrifugo":"2.0.0","roadrunner-php\/roadrunner-api-dto":"1.4.0","sebastian\/cli-parser":"1.0.1","sebastian\/code-unit":"1.0.8","sebastian\/code-unit-reverse-lookup":"2.0.3","sebastian\/comparator":"4.0.8","sebastian\/complexity":"2.0.2","sebastian\/diff":"4.0.5","sebastian\/environment":"5.1.5","sebastian\/exporter":"4.0.5","sebastian\/global-state":"5.0.6","sebastian\/lines-of-code":"1.0.3","sebastian\/object-enumerator":"4.0.4","sebastian\/object-reflector":"2.0.4","sebastian\/recursion-context":"4.0.5","sebastian\/resource-operations":"3.0.3","sebastian\/type":"3.2.1","sebastian\/version":"3.0.2","sentry\/sdk":"4.0.0","sentry\/sentry":"4.0.1","spatie\/array-to-xml":"3.2.2","spiral-packages\/cqrs":"v2.3.0","spiral-packages\/league-event":"1.0.1","spiral\/attributes":"v3.1.2","spiral\/composer-publish-plugin":"v1.1.2","spiral\/cycle-bridge":"v2.8.0","spiral\/data-grid":"v3.0.0","spiral\/data-grid-bridge":"v3.0.1","spiral\/framework":"3.10.0","spiral\/goridge":"4.1.0","spiral\/nyholm-bridge":"v1.3.0","spiral\/roadrunner":"v2023.3.7","spiral\/roadrunner-bridge":"3.0.2","spiral\/roadrunner-grpc":"3.2.0","spiral\/roadrunner-http":"3.2.0","spiral\/roadrunner-jobs":"4.3.0","spiral\/roadrunner-kv":"4.0.0","spiral\/roadrunner-metrics":"3.1.0","spiral\/roadrunner-services":"2.1.0","spiral\/roadrunner-tcp":"3.0.0","spiral\/roadrunner-worker":"3.2.0","spiral\/testing":"2.6.2","spiral\/validator":"1.5.0","symfony\/clock":"v7.0.0","symfony\/console":"v6.4.1","symfony\/deprecation-contracts":"v3.4.0","symfony\/event-dispatcher":"v7.0.0","symfony\/event-dispatcher-contracts":"v3.4.0","symfony\/filesystem":"v7.0.0","symfony\/finder":"v6.4.0","symfony\/mailer":"v6.4.0","symfony\/messenger":"v6.4.0","symfony\/mime":"v6.4.0","symfony\/options-resolver":"v7.0.0","symfony\/polyfill-ctype":"v1.28.0","symfony\/polyfill-iconv":"v1.28.0","symfony\/polyfill-intl-grapheme":"v1.28.0","symfony\/polyfill-intl-idn":"v1.28.0","symfony\/polyfill-intl-normalizer":"v1.28.0","symfony\/polyfill-mbstring":"v1.28.0","symfony\/polyfill-php72":"v1.28.0","symfony\/polyfill-php80":"v1.28.0","symfony\/polyfill-php83":"v1.28.0","symfony\/process":"v6.4.0","symfony\/service-contracts":"v3.4.0","symfony\/string":"v7.0.0","symfony\/translation":"v6.4.0","symfony\/translation-contracts":"v3.4.0","symfony\/var-dumper":"v6.4.0","symfony\/yaml":"v7.0.0","theseer\/tokenizer":"1.2.2","vimeo\/psalm":"5.16.0","vlucas\/phpdotenv":"v5.6.0","webmozart\/assert":"1.11.0","yiisoft\/friendly-exception":"1.1.0","zbateson\/mail-mime-parser":"2.4.0","zbateson\/mb-wrapper":"1.2.0","zbateson\/stream-decorators":"1.2.1","zentlix\/swagger-php":"1.x-dev@1f4927a","zircote\/swagger-php":"4.7.16"},"contexts":{"os":{"name":"Linux","version":"5.15.133.1-microsoft-standard-WSL2","build":"#1 SMP Thu Oct 5 21:02:42 UTC 2023","kernel_version":"Linux Test 5.15.133.1-microsoft-standard-WSL2 #1 SMP Thu Oct 5 21:02:42 UTC 2023 x86_64"},"runtime":{"name":"php","version":"8.2.5"},"trace":{"trace_id":"143ef743ce184eb7abd0ae0891d33b7d","span_id":"e4a276672c8a4a38"}},"exception":{"values":[{"type":"Exception","value":"test","stacktrace":{"frames":[{"filename":"\/vendor\/phpunit\/phpunit\/phpunit","lineno":107,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/phpunit","pre_context":["","unset($options);","","require PHPUNIT_COMPOSER_INSTALL;",""],"context_line":"PHPUnit\\TextUI\\Command::main();","post_context":[""]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","lineno":97,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","function":"PHPUnit\\TextUI\\Command::main","raw_function":"PHPUnit\\TextUI\\Command::main","pre_context":["     * @throws Exception","     *\/","    public static function main(bool $exit = true): int","    {","        try {"],"context_line":"            return (new static)->run($_SERVER['argv'], $exit);","post_context":["        } catch (Throwable $t) {","            throw new RuntimeException(","                $t->getMessage(),","                (int) $t->getCode(),","                $t,"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","lineno":144,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","function":"PHPUnit\\TextUI\\Command::run","raw_function":"PHPUnit\\TextUI\\Command::run","pre_context":["        }","","        unset($this->arguments['test'], $this->arguments['testFile']);","","        try {"],"context_line":"            $result = $runner->run($suite, $this->arguments, $this->warnings, $exit);","post_context":["        } catch (Throwable $t) {","            print $t->getMessage() . PHP_EOL;","        }","","        $return = TestRunner::FAILURE_EXIT;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","lineno":651,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","function":"PHPUnit\\TextUI\\TestRunner::run","raw_function":"PHPUnit\\TextUI\\TestRunner::run","pre_context":["            if ($extension instanceof BeforeFirstTestHook) {","                $extension->executeBeforeFirstTest();","            }","        }",""],"context_line":"        $suite->run($result);","post_context":["","        foreach ($this->extensions as $extension) {","            if ($extension instanceof AfterLastTestHook) {","                $extension->executeAfterLastTest();","            }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":["                $test->setBackupGlobals($this->backupGlobals);","                $test->setBackupStaticAttributes($this->backupStaticAttributes);","                $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);","            }",""],"context_line":"            $test->run($result);","post_context":["        }","","        if ($this->testCase && class_exists($this->name, false)) {","            foreach ($hookMethods['afterClass'] as $afterClassMethod) {","                if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":["                $test->setBackupGlobals($this->backupGlobals);","                $test->setBackupStaticAttributes($this->backupStaticAttributes);","                $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);","            }",""],"context_line":"            $test->run($result);","post_context":["        }","","        if ($this->testCase && class_exists($this->name, false)) {","            foreach ($hookMethods['afterClass'] as $afterClassMethod) {","                if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":968,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::run","raw_function":"PHPUnit\\Framework\\TestCase::run","pre_context":["            $template->setVar($var);","","            $php = AbstractPhpProcess::factory();","            $php->runTestJob($template->render(), $this, $result, $processResultFile);","        } else {"],"context_line":"            $result->run($this);","post_context":["        }","","        $this->result = null;","","        return $result;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","lineno":728,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","function":"PHPUnit\\Framework\\TestResult::run","raw_function":"PHPUnit\\Framework\\TestResult::run","pre_context":["                        $_timeout = $this->defaultTimeLimit;","                }","","                $invoker->invoke([$test, 'runBare'], [], $_timeout);","            } else {"],"context_line":"                $test->runBare();","post_context":["            }","        } catch (TimeoutException $e) {","            $this->addFailure(","                $test,","                new RiskyTestError("]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1218,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runBare","raw_function":"PHPUnit\\Framework\\TestCase::runBare","pre_context":["","            foreach ($hookMethods['preCondition'] as $method) {","                $this->{$method}();","            }",""],"context_line":"            $this->testResult = $this->runTest();","post_context":["            $this->verifyMockObjects();","","            foreach ($hookMethods['postCondition'] as $method) {","                $this->{$method}();","            }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1612,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runTest","raw_function":"PHPUnit\\Framework\\TestCase::runTest","pre_context":["        $testArguments = array_merge($this->data, $this->dependencyInput);","","        $this->registerMockObjectsFromTestArguments($testArguments);","","        try {"],"context_line":"            $testResult = $this->{$this->name}(...array_values($testArguments));","post_context":["        } catch (Throwable $exception) {","            if (!$this->checkExceptionExpectations($exception)) {","                throw $exception;","            }",""]},{"filename":"\/tests\/Feature\/Interfaces\/Http\/Sentry\/SentryV4ActionTest.php","lineno":14,"in_app":true,"abs_path":"\\/tests\/Feature\/Interfaces\/Http\/Sentry\/SentryV4ActionTest.php","function":"Interfaces\\Http\\Sentry\\SentryV4ActionTest::testSend","raw_function":"Interfaces\\Http\\Sentry\\SentryV4ActionTest::testSend","pre_context":["final class SentryV4ActionTest extends ControllerTestCase","{","    public function testSend(): void","    {","        \\Sentry\\init(['dsn' => 'http:\/\/user@127.0.0.1:8082\/1']);"],"context_line":"        \\Sentry\\captureException(new \\Exception('test'));","post_context":["","\/\/        $this->http","\/\/            ->postJson(","\/\/                uri: '\/api\/1\/store\/',","\/\/                data: Stream::create("]}]},"mechanism":{"type":"generic","handled":true,"data":{"code":0}}}]}}
+{"event_id":"2b4f7918973f4371933dce5b3ac381bd","timestamp":1701455435.634665,"platform":"php","sdk":{"name":"sentry.php","version":"4.0.1"},"server_name":"Test","environment":"production","modules":{"amphp\/amp":"v2.6.2","amphp\/byte-stream":"v1.8.1","brick\/math":"0.11.0","buggregator\/app":"dev-master@818ea82","clue\/stream-filter":"v1.6.0","cocur\/slugify":"v3.2","codedungeon\/php-cli-colors":"1.12.2","composer\/pcre":"3.1.1","composer\/semver":"3.4.0","composer\/xdebug-handler":"3.0.3","cycle\/annotated":"v3.4.0","cycle\/database":"2.6.1","cycle\/migrations":"v4.2.1","cycle\/orm":"v2.5.0","cycle\/schema-builder":"v2.6.1","cycle\/schema-migrations-generator":"2.2.0","cycle\/schema-renderer":"1.2.0","defuse\/php-encryption":"v2.4.0","dnoegel\/php-xdg-base-dir":"v0.1.1","doctrine\/annotations":"2.0.1","doctrine\/collections":"1.8.0","doctrine\/deprecations":"1.1.2","doctrine\/inflector":"2.0.8","doctrine\/instantiator":"2.0.0","doctrine\/lexer":"3.0.0","egulias\/email-validator":"4.0.2","felixfbecker\/advanced-json-rpc":"v3.2.1","felixfbecker\/language-server-protocol":"v1.5.2","fidry\/cpu-core-counter":"0.5.1","google\/common-protos":"v4.5.0","google\/protobuf":"v3.25.1","graham-campbell\/result-type":"v1.1.2","grpc\/grpc":"1.57.0","guzzlehttp\/psr7":"2.6.1","hamcrest\/hamcrest-php":"v2.0.1","jean85\/pretty-package-versions":"2.0.5","league\/event":"3.0.2","league\/flysystem":"2.5.0","league\/mime-type-detection":"1.14.0","mockery\/mockery":"1.6.6","monolog\/monolog":"2.9.2","myclabs\/deep-copy":"1.11.1","nesbot\/carbon":"2.71.0","netresearch\/jsonmapper":"v4.2.0","nette\/php-generator":"v4.1.2","nette\/utils":"v4.0.3","nikic\/php-parser":"v4.17.1","nyholm\/psr7":"1.8.1","paragonie\/random_compat":"v9.99.100","phar-io\/manifest":"2.0.3","phar-io\/version":"3.2.1","php-http\/message":"1.16.0","phpdocumentor\/reflection-common":"2.2.0","phpdocumentor\/reflection-docblock":"5.3.0","phpdocumentor\/type-resolver":"1.7.3","phpoption\/phpoption":"1.9.2","phpstan\/phpdoc-parser":"1.24.4","phpunit\/php-code-coverage":"9.2.29","phpunit\/php-file-iterator":"3.0.6","phpunit\/php-invoker":"3.1.1","phpunit\/php-text-template":"2.0.4","phpunit\/php-timer":"5.0.3","phpunit\/phpunit":"9.6.15","pimple\/pimple":"v3.5.0","psr\/cache":"3.0.0","psr\/clock":"1.0.0","psr\/container":"2.0.2","psr\/event-dispatcher":"1.0.0","psr\/http-factory":"1.0.2","psr\/http-message":"2.0","psr\/http-server-handler":"1.0.2","psr\/http-server-middleware":"1.0.2","psr\/log":"3.0.0","psr\/simple-cache":"3.0.0","qossmic\/deptrac-shim":"1.0.2","ralouphie\/getallheaders":"3.0.3","ramsey\/collection":"2.0.0","ramsey\/uuid":"4.7.5","roadrunner-php\/app-logger":"1.1.0","roadrunner-php\/centrifugo":"2.0.0","roadrunner-php\/roadrunner-api-dto":"1.4.0","sebastian\/cli-parser":"1.0.1","sebastian\/code-unit":"1.0.8","sebastian\/code-unit-reverse-lookup":"2.0.3","sebastian\/comparator":"4.0.8","sebastian\/complexity":"2.0.2","sebastian\/diff":"4.0.5","sebastian\/environment":"5.1.5","sebastian\/exporter":"4.0.5","sebastian\/global-state":"5.0.6","sebastian\/lines-of-code":"1.0.3","sebastian\/object-enumerator":"4.0.4","sebastian\/object-reflector":"2.0.4","sebastian\/recursion-context":"4.0.5","sebastian\/resource-operations":"3.0.3","sebastian\/type":"3.2.1","sebastian\/version":"3.0.2","sentry\/sdk":"4.0.0","sentry\/sentry":"4.0.1","spatie\/array-to-xml":"3.2.2","spiral-packages\/cqrs":"v2.3.0","spiral-packages\/league-event":"1.0.1","spiral\/attributes":"v3.1.2","spiral\/composer-publish-plugin":"v1.1.2","spiral\/cycle-bridge":"v2.8.0","spiral\/data-grid":"v3.0.0","spiral\/data-grid-bridge":"v3.0.1","spiral\/framework":"3.10.0","spiral\/goridge":"4.1.0","spiral\/nyholm-bridge":"v1.3.0","spiral\/roadrunner":"v2023.3.7","spiral\/roadrunner-bridge":"3.0.2","spiral\/roadrunner-grpc":"3.2.0","spiral\/roadrunner-http":"3.2.0","spiral\/roadrunner-jobs":"4.3.0","spiral\/roadrunner-kv":"4.0.0","spiral\/roadrunner-metrics":"3.1.0","spiral\/roadrunner-services":"2.1.0","spiral\/roadrunner-tcp":"3.0.0","spiral\/roadrunner-worker":"3.2.0","spiral\/testing":"2.6.2","spiral\/validator":"1.5.0","symfony\/clock":"v7.0.0","symfony\/console":"v6.4.1","symfony\/deprecation-contracts":"v3.4.0","symfony\/event-dispatcher":"v7.0.0","symfony\/event-dispatcher-contracts":"v3.4.0","symfony\/filesystem":"v7.0.0","symfony\/finder":"v6.4.0","symfony\/mailer":"v6.4.0","symfony\/messenger":"v6.4.0","symfony\/mime":"v6.4.0","symfony\/options-resolver":"v7.0.0","symfony\/polyfill-ctype":"v1.28.0","symfony\/polyfill-iconv":"v1.28.0","symfony\/polyfill-intl-grapheme":"v1.28.0","symfony\/polyfill-intl-idn":"v1.28.0","symfony\/polyfill-intl-normalizer":"v1.28.0","symfony\/polyfill-mbstring":"v1.28.0","symfony\/polyfill-php72":"v1.28.0","symfony\/polyfill-php80":"v1.28.0","symfony\/polyfill-php83":"v1.28.0","symfony\/process":"v6.4.0","symfony\/service-contracts":"v3.4.0","symfony\/string":"v7.0.0","symfony\/translation":"v6.4.0","symfony\/translation-contracts":"v3.4.0","symfony\/var-dumper":"v6.4.0","symfony\/yaml":"v7.0.0","theseer\/tokenizer":"1.2.2","vimeo\/psalm":"5.16.0","vlucas\/phpdotenv":"v5.6.0","webmozart\/assert":"1.11.0","yiisoft\/friendly-exception":"1.1.0","zbateson\/mail-mime-parser":"2.4.0","zbateson\/mb-wrapper":"1.2.0","zbateson\/stream-decorators":"1.2.1","zentlix\/swagger-php":"1.x-dev@1f4927a","zircote\/swagger-php":"4.7.16"},"contexts":{"os":{"name":"Linux","version":"5.15.133.1-microsoft-standard-WSL2","build":"#1 SMP Thu Oct 5 21:02:42 UTC 2023","kernel_version":"Linux Test 5.15.133.1-microsoft-standard-WSL2 #1 SMP Thu Oct 5 21:02:42 UTC 2023 x86_64"},"runtime":{"name":"php","version":"8.2.5"},"trace":{"trace_id":"143ef743ce184eb7abd0ae0891d33b7d","span_id":"e4a276672c8a4a38"}},"exception":{"values":[{"type":"Exception","value":"test","stacktrace":{"frames":[{"filename":"\/vendor\/phpunit\/phpunit\/phpunit","lineno":107,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/phpunit","pre_context":["","unset($options);","","require PHPUNIT_COMPOSER_INSTALL;",""],"context_line":"PHPUnit\\TextUI\\Command::main();","post_context":[""]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","lineno":97,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","function":"PHPUnit\\TextUI\\Command::main","raw_function":"PHPUnit\\TextUI\\Command::main","pre_context":["     * @throws Exception","     *\/","    public static function main(bool $exit = true): int","    {","        try {"],"context_line":"            return (new static)->run($_SERVER['argv'], $exit);","post_context":["        } catch (Throwable $t) {","            throw new RuntimeException(","                $t->getMessage(),","                (int) $t->getCode(),","                $t,"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","lineno":144,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/Command.php","function":"PHPUnit\\TextUI\\Command::run","raw_function":"PHPUnit\\TextUI\\Command::run","pre_context":["        }","","        unset($this->arguments['test'], $this->arguments['testFile']);","","        try {"],"context_line":"            $result = $runner->run($suite, $this->arguments, $this->warnings, $exit);","post_context":["        } catch (Throwable $t) {","            print $t->getMessage() . PHP_EOL;","        }","","        $return = TestRunner::FAILURE_EXIT;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","lineno":651,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/TextUI\/TestRunner.php","function":"PHPUnit\\TextUI\\TestRunner::run","raw_function":"PHPUnit\\TextUI\\TestRunner::run","pre_context":["            if ($extension instanceof BeforeFirstTestHook) {","                $extension->executeBeforeFirstTest();","            }","        }",""],"context_line":"        $suite->run($result);","post_context":["","        foreach ($this->extensions as $extension) {","            if ($extension instanceof AfterLastTestHook) {","                $extension->executeAfterLastTest();","            }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":["                $test->setBackupGlobals($this->backupGlobals);","                $test->setBackupStaticAttributes($this->backupStaticAttributes);","                $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);","            }",""],"context_line":"            $test->run($result);","post_context":["        }","","        if ($this->testCase && class_exists($this->name, false)) {","            foreach ($hookMethods['afterClass'] as $afterClassMethod) {","                if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","lineno":684,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestSuite.php","function":"PHPUnit\\Framework\\TestSuite::run","raw_function":"PHPUnit\\Framework\\TestSuite::run","pre_context":["                $test->setBackupGlobals($this->backupGlobals);","                $test->setBackupStaticAttributes($this->backupStaticAttributes);","                $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess);","            }",""],"context_line":"            $test->run($result);","post_context":["        }","","        if ($this->testCase && class_exists($this->name, false)) {","            foreach ($hookMethods['afterClass'] as $afterClassMethod) {","                if (method_exists($this->name, $afterClassMethod)) {"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":968,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::run","raw_function":"PHPUnit\\Framework\\TestCase::run","pre_context":["            $template->setVar($var);","","            $php = AbstractPhpProcess::factory();","            $php->runTestJob($template->render(), $this, $result, $processResultFile);","        } else {"],"context_line":"            $result->run($this);","post_context":["        }","","        $this->result = null;","","        return $result;"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","lineno":728,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestResult.php","function":"PHPUnit\\Framework\\TestResult::run","raw_function":"PHPUnit\\Framework\\TestResult::run","pre_context":["                        $_timeout = $this->defaultTimeLimit;","                }","","                $invoker->invoke([$test, 'runBare'], [], $_timeout);","            } else {"],"context_line":"                $test->runBare();","post_context":["            }","        } catch (TimeoutException $e) {","            $this->addFailure(","                $test,","                new RiskyTestError("]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1218,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runBare","raw_function":"PHPUnit\\Framework\\TestCase::runBare","pre_context":["","            foreach ($hookMethods['preCondition'] as $method) {","                $this->{$method}();","            }",""],"context_line":"            $this->testResult = $this->runTest();","post_context":["            $this->verifyMockObjects();","","            foreach ($hookMethods['postCondition'] as $method) {","                $this->{$method}();","            }"]},{"filename":"\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","lineno":1612,"in_app":true,"abs_path":"\\/vendor\/phpunit\/phpunit\/src\/Framework\/TestCase.php","function":"PHPUnit\\Framework\\TestCase::runTest","raw_function":"PHPUnit\\Framework\\TestCase::runTest","pre_context":["        $testArguments = array_merge($this->data, $this->dependencyInput);","","        $this->registerMockObjectsFromTestArguments($testArguments);","","        try {"],"context_line":"            $testResult = $this->{$this->name}(...array_values($testArguments));","post_context":["        } catch (Throwable $exception) {","            if (!$this->checkExceptionExpectations($exception)) {","                throw $exception;","            }",""]},{"filename":"\/tests\/Feature\/Interfaces\/Http\/Sentry\/SentryV4ActionTest.php","lineno":14,"in_app":true,"abs_path":"\\/tests\/Feature\/Interfaces\/Http\/Sentry\/SentryV4ActionTest.php","function":"Interfaces\\Http\\Sentry\\SentryV4ActionTest::testSend","raw_function":"Interfaces\\Http\\Sentry\\SentryV4ActionTest::testSend","pre_context":["final class SentryV4ActionTest extends ControllerTestCase","{","    public function testSend(): void","    {","        \\Sentry\\init(['dsn' => 'http:\/\/user@127.0.0.1:8082\/1']);"],"context_line":"        \\Sentry\\captureException(new \\Exception('test'));","post_context":["","\/\/        $this->http","\/\/            ->postJson(","\/\/                uri: '\/api\/1\/store\/',","\/\/                data: Stream::create("]}]},"mechanism":{"type":"generic","handled":true,"data":{"code":0}}}]}}
 BODY;
 
     private Project $project;
diff --git a/tests/Feature/Interfaces/Http/Sentry/SentryVueEventActionTest.php b/tests/Feature/Interfaces/Http/Sentry/SentryVueEventActionTest.php
index e5232cd7..72cbf872 100644
--- a/tests/Feature/Interfaces/Http/Sentry/SentryVueEventActionTest.php
+++ b/tests/Feature/Interfaces/Http/Sentry/SentryVueEventActionTest.php
@@ -33,6 +33,7 @@ public function testSend(): void
         $this->makeRequest(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']);