diff --git a/agent/src/Envelope.php b/agent/src/Envelope.php index e6eb8ff..600f969 100644 --- a/agent/src/Envelope.php +++ b/agent/src/Envelope.php @@ -77,6 +77,13 @@ static function (EnvelopeItem $item) use ($callback) { ); } + public function appendIngestPath(string $version): void + { + foreach ($this->items as $item) { + $item->appendIngestPath($version); + } + } + public function __toString() { $data = implode( diff --git a/agent/src/EnvelopeForwarder.php b/agent/src/EnvelopeForwarder.php index 254225e..74ab556 100644 --- a/agent/src/EnvelopeForwarder.php +++ b/agent/src/EnvelopeForwarder.php @@ -83,9 +83,12 @@ public function forward(Envelope $envelope): PromiseInterface return resolve(null); } + $client = self::IDENTIFIER . '/' . self::VERSION; + $envelope->appendIngestPath($client); + $authHeader = [ 'sentry_version=' . self::PROTOCOL_VERSION, - 'sentry_client=' . self::IDENTIFIER . '/' . self::VERSION, + 'sentry_client=' . $client, 'sentry_key=' . $dsn->getPublicKey(), ]; @@ -95,7 +98,7 @@ public function forward(Envelope $envelope): PromiseInterface return (new Browser())->withTimeout($this->timeout)->post( $dsn->getEnvelopeApiEndpointUrl(), [ - 'User-Agent' => self::IDENTIFIER . '/' . self::VERSION, + 'User-Agent' => $client, 'Content-Type' => Envelope::CONTENT_TYPE, 'X-Sentry-Auth' => 'Sentry ' . implode(', ', $authHeader), ], diff --git a/agent/src/EnvelopeItem.php b/agent/src/EnvelopeItem.php index c850553..46f7b5f 100644 --- a/agent/src/EnvelopeItem.php +++ b/agent/src/EnvelopeItem.php @@ -4,15 +4,23 @@ namespace Sentry\Agent; +use Sentry\Util\JSON; + /** * @internal * * @phpstan-type EnvelopeItemHeader array{ * type: string, + * length?: int, * } */ class EnvelopeItem { + private const EVENT_ITEM_TYPES_WITH_INGEST_PATH = [ + 'event' => true, + 'transaction' => true, + ]; + /** * @var EnvelopeItemHeader The envelope item header */ @@ -46,6 +54,39 @@ public function getData(): string return $this->data; } + public function appendIngestPath(string $version): void + { + if (!isset(self::EVENT_ITEM_TYPES_WITH_INGEST_PATH[$this->header['type']])) { + return; + } + + $payload = json_decode($this->data, true); + + if (!\is_array($payload)) { + return; + } + + if (!isset($payload['ingest_path']) || !\is_array($payload['ingest_path'])) { + $payload['ingest_path'] = []; + } + + $payload['ingest_path'][] = [ + 'version' => $version, + ]; + + try { + $data = JSON::encode($payload); + } catch (\Throwable $e) { + return; + } + + $this->data = $data; + + if (isset($this->header['length'])) { + $this->header['length'] = \strlen($this->data); + } + } + public function __toString() { return json_encode($this->header) . "\n" . $this->data; diff --git a/agent/tests/AgentForwardingTest.php b/agent/tests/AgentForwardingTest.php index 4698e42..ab65298 100644 --- a/agent/tests/AgentForwardingTest.php +++ b/agent/tests/AgentForwardingTest.php @@ -5,6 +5,7 @@ namespace Sentry\Agent\Tests; use PHPUnit\Framework\TestCase; +use Sentry\Agent\EnvelopeForwarder; use Sentry\Event; use Sentry\Options; use Sentry\Serializer\PayloadSerializer; @@ -32,6 +33,10 @@ public function testAgentForwardsEnvelopeToUpstream(): void $this->assertEquals(1, $serverOutput['request_count']); $this->assertStringContainsString('Hello from agent test!', $serverOutput['body']); $this->assertStringContainsString('"type":"event"', $serverOutput['body']); + $this->assertStringContainsString( + '"ingest_path":[{"version":"' . str_replace('/', '\/', EnvelopeForwarder::IDENTIFIER . '/' . EnvelopeForwarder::VERSION) . '"}]', + $serverOutput['body'] + ); // Verify the correct headers were sent $this->assertArrayHasKey('X-Sentry-Auth', $serverOutput['headers']); diff --git a/agent/tests/EnvelopeTest.php b/agent/tests/EnvelopeTest.php index 92daf53..6ad6e14 100644 --- a/agent/tests/EnvelopeTest.php +++ b/agent/tests/EnvelopeTest.php @@ -6,6 +6,8 @@ use PHPUnit\Framework\TestCase; use Sentry\Agent\Envelope; +use Sentry\Agent\EnvelopeForwarder; +use Sentry\Agent\EnvelopeItem; class EnvelopeTest extends TestCase { @@ -51,6 +53,99 @@ public function testCanParseEnvelopeWith2Items(): void $this->assertEquals($payload, (string) $envelope); } + public function testAppendIngestPathToEventItem(): void + { + $envelope = new Envelope( + ['dsn' => 'http://public@example.com/1'], + [new EnvelopeItem(['type' => 'event'], '{"message":"test"}')] + ); + + $envelope->appendIngestPath($this->getIngestPathVersion()); + + $payload = json_decode($envelope->getItems()[0]->getData(), true); + + $this->assertSame([['version' => $this->getIngestPathVersion()]], $payload['ingest_path']); + } + + public function testAppendIngestPathPreservesExistingEntries(): void + { + $envelope = new Envelope( + ['dsn' => 'http://public@example.com/1'], + [ + new EnvelopeItem( + ['type' => 'transaction'], + '{"transaction":"/test","ingest_path":[{"version":"relay/1.0.0","public_key":"abc"}]}' + ), + ] + ); + + $envelope->appendIngestPath($this->getIngestPathVersion()); + + $payload = json_decode($envelope->getItems()[0]->getData(), true); + + $this->assertSame( + [ + ['version' => 'relay/1.0.0', 'public_key' => 'abc'], + ['version' => $this->getIngestPathVersion()], + ], + $payload['ingest_path'] + ); + } + + public function testAppendIngestPathUpdatesLengthHeader(): void + { + $envelope = new Envelope( + ['dsn' => 'http://public@example.com/1'], + [new EnvelopeItem(['type' => 'event', 'length' => 18], '{"message":"test"}')] + ); + + $envelope->appendIngestPath($this->getIngestPathVersion()); + + $item = $envelope->getItems()[0]; + + $this->assertSame(\strlen($item->getData()), $item->getHeader()['length']); + } + + public function testAppendIngestPathReplacesInvalidExistingIngestPath(): void + { + $envelope = new Envelope( + ['dsn' => 'http://public@example.com/1'], + [new EnvelopeItem(['type' => 'event'], '{"message":"test","ingest_path":"invalid"}')] + ); + + $envelope->appendIngestPath($this->getIngestPathVersion()); + + $payload = json_decode($envelope->getItems()[0]->getData(), true); + + $this->assertSame([['version' => $this->getIngestPathVersion()]], $payload['ingest_path']); + } + + public function testAppendIngestPathDoesNotMutateUnsupportedItems(): void + { + foreach (['log', 'check_in', 'profile', 'attachment'] as $type) { + $envelope = new Envelope( + ['dsn' => 'http://public@example.com/1'], + [new EnvelopeItem(['type' => $type], '{"message":"test"}')] + ); + + $envelope->appendIngestPath($this->getIngestPathVersion()); + + $this->assertSame('{"message":"test"}', $envelope->getItems()[0]->getData()); + } + } + + public function testAppendIngestPathDoesNotMutateMalformedJson(): void + { + $envelope = new Envelope( + ['dsn' => 'http://public@example.com/1'], + [new EnvelopeItem(['type' => 'event'], '{"message":')] + ); + + $envelope->appendIngestPath($this->getIngestPathVersion()); + + $this->assertSame('{"message":', $envelope->getItems()[0]->getData()); + } + private function getFixture(string $name): string { $fixture = file_get_contents(__DIR__ . "/fixtures/envelopes/{$name}.dat"); @@ -62,4 +157,9 @@ private function getFixture(string $name): string // To make it easier to edit the fixtures, we remove the trailing new line return rtrim($fixture, "\n"); } + + private function getIngestPathVersion(): string + { + return EnvelopeForwarder::IDENTIFIER . '/' . EnvelopeForwarder::VERSION; + } } diff --git a/bin/sentry-agent b/bin/sentry-agent index 07aea35..2018297 100755 Binary files a/bin/sentry-agent and b/bin/sentry-agent differ diff --git a/bin/sentry-agent.sig b/bin/sentry-agent.sig index 3f96e06..cee0682 100644 --- a/bin/sentry-agent.sig +++ b/bin/sentry-agent.sig @@ -1 +1 @@ -C5A1B020D608BDEF9E7D690A7DA18DF1BE0F3FF534304B3CED4734096A415F63B448F8156224E80F530CF203582C124CE04F4BABF968F5A905DA83199B5F6EF9 +D8B94BAD7E5B61D6310089C043AA6B9A3BEFD4FB4B41752004863AB4B822A2A43B49CC2587FB83446D8AA5AC8FE6A5AA6095D8DC932DE00334D1359F70C1D9A8