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']);