diff --git a/app/config/migration.php b/app/config/migration.php index 7fc6525..3a41901 100644 --- a/app/config/migration.php +++ b/app/config/migration.php @@ -5,20 +5,13 @@ 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' => 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 0000000..cb31233 --- /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 0000000..59b3fe1 --- /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 0000000..a16598d --- /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 0000000..1f5ede6 --- /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 0000000..239d6ba --- /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 a07e4f3..617d599 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 e40eaae..d307a7f 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 fe4b367..0ada116 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())) !== null) { - $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 012ac9b..33187f7 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 45a2d38..9d54964 100644 --- a/app/modules/Ray/Application/Handlers/MergeEventsHandler.php +++ b/app/modules/Ray/Application/Handlers/MergeEventsHandler.php @@ -8,7 +8,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 b67c5fc..e44fc0f 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 d77f78a..d30516b 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 0000000..7789c56
--- /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 0000000..2be12ff
--- /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 0000000..4c03f79
--- /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 0000000..de757b1
--- /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 b236a7a..402edba 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 0000000..4a66f18
--- /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 0000000..32bb729
--- /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 1230eaf..775510f 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 0000000..13cdc3a
--- /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 0000000..04bcbe2
--- /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 cb75f04..8f05c5b 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 f0fb8d5..eac8d9a 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 d3617ed..b2c1146 100644
--- a/app/modules/Sentry/Application/SentryBootloader.php
+++ b/app/modules/Sentry/Application/SentryBootloader.php
@@ -6,7 +6,30 @@
use Modules\Sentry\Application\Mapper\EventTypeMapper;
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;
@@ -24,7 +47,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 0000000..38c51d8
--- /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 0000000..df94c6b
--- /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 0000000..4f70d21
--- /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 0000000..b32d9a9
--- /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 0000000..f502c30
--- /dev/null
+++ b/app/modules/Sentry/Domain/TraceFactoryInterface.php
@@ -0,0 +1,16 @@
+[] $handlers
@@ -17,12 +19,12 @@ public function __construct(
private array $handlers,
) {}
- public function handle(array $event): array
+ public function handle(Payload $payload, EventType $event): Payload
{
foreach ($this->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 0000000..bd787ba
--- /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 0000000..ede94ea
--- /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 0000000..c6947c7
--- /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 0000000..eba5595
--- /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 0000000..64b89ba
--- /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 0000000..428f4a6
--- /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 5f01a6b..8d5b813 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 0000000..eae34f5
--- /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 0000000..e86574a
--- /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 636952e..6a9cd35 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 56fd598..a123ea9 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 0000000..3adffce
--- /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 9c5d006..0000000
--- a/tests/Feature/Interfaces/Http/Sentry/SentryV3ActionTest.php
+++ /dev/null
@@ -1,89 +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 b94a2ae..0097339 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 e5232cd..72cbf87 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']);