diff --git a/agent/src/Envelope.php b/agent/src/Envelope.php index 600f969..6990003 100644 --- a/agent/src/Envelope.php +++ b/agent/src/Envelope.php @@ -77,10 +77,10 @@ static function (EnvelopeItem $item) use ($callback) { ); } - public function appendIngestPath(string $version): void + public function prepareForForwarding(string $client): void { foreach ($this->items as $item) { - $item->appendIngestPath($version); + $item->prepareForForwarding($client); } } diff --git a/agent/src/EnvelopeForwarder.php b/agent/src/EnvelopeForwarder.php index 3f4a3fc..6f9a86f 100644 --- a/agent/src/EnvelopeForwarder.php +++ b/agent/src/EnvelopeForwarder.php @@ -92,7 +92,7 @@ public function forward(Envelope $envelope): PromiseInterface } $client = self::IDENTIFIER . '/' . self::VERSION; - $envelope->appendIngestPath($client); + $envelope->prepareForForwarding($client); $authHeader = [ 'sentry_version=' . self::PROTOCOL_VERSION, diff --git a/agent/src/EnvelopeItem.php b/agent/src/EnvelopeItem.php index 46f7b5f..20485e0 100644 --- a/agent/src/EnvelopeItem.php +++ b/agent/src/EnvelopeItem.php @@ -16,10 +16,7 @@ */ class EnvelopeItem { - private const EVENT_ITEM_TYPES_WITH_INGEST_PATH = [ - 'event' => true, - 'transaction' => true, - ]; + public const TRANSPORT_KEY = 'sentry.php.agent'; /** * @var EnvelopeItemHeader The envelope item header @@ -54,37 +51,79 @@ public function getData(): string return $this->data; } - public function appendIngestPath(string $version): void + public function prepareForForwarding(string $client): void { - if (!isset(self::EVENT_ITEM_TYPES_WITH_INGEST_PATH[$this->header['type']])) { + try { + switch ($this->header['type']) { + case 'event': + case 'transaction': + /** @var array $payload */ + $payload = JSON::decode($this->data); + $payload = self::addIngestPathAndTransportTag($payload, $client); + break; + case 'log': + case 'trace_metric': + /** @var array $payload */ + $payload = JSON::decode($this->data); + $payload = self::addTransportAttributeToItems($payload); + break; + default: + return; + } + + $data = JSON::encode($payload); + } catch (\Throwable $e) { return; } - $payload = json_decode($this->data, true); + $this->data = $data; - if (!\is_array($payload)) { - return; + if (isset($this->header['length'])) { + $this->header['length'] = \strlen($this->data); } + } + /** + * @param array $payload + * + * @return array + */ + private static function addIngestPathAndTransportTag(array $payload, string $client): array + { if (!isset($payload['ingest_path']) || !\is_array($payload['ingest_path'])) { $payload['ingest_path'] = []; } $payload['ingest_path'][] = [ - 'version' => $version, + 'version' => $client, ]; - try { - $data = JSON::encode($payload); - } catch (\Throwable $e) { - return; + if (!isset($payload['tags']) || !\is_array($payload['tags'])) { + $payload['tags'] = []; } - $this->data = $data; + $payload['tags'][self::TRANSPORT_KEY] = 'true'; - if (isset($this->header['length'])) { - $this->header['length'] = \strlen($this->data); + return $payload; + } + + /** + * @param array $payload + * + * @return array + */ + private static function addTransportAttributeToItems(array $payload): array + { + foreach ($payload['items'] ?? [] as $index => $item) { + $item['attributes'][self::TRANSPORT_KEY] = [ + 'type' => 'bool', + 'value' => true, + ]; + + $payload['items'][$index] = $item; } + + return $payload; } public function __toString() diff --git a/agent/src/sentry-agent.php b/agent/src/sentry-agent.php index fa502bd..bbe8340 100755 --- a/agent/src/sentry-agent.php +++ b/agent/src/sentry-agent.php @@ -68,7 +68,7 @@ function printHelp(): void HELP; } -$options = getopt('h', ['listen::', 'upstream-timeout::', 'upstream-concurrency::', 'queue-limit::', 'drain-timeout::', 'control-server::', 'http-proxy::', 'http-proxy-authentication::', 'help']); +$options = getopt('hv', ['listen::', 'upstream-timeout::', 'upstream-concurrency::', 'queue-limit::', 'drain-timeout::', 'control-server::', 'http-proxy::', 'http-proxy-authentication::', 'help', 'verbose']); if ($options === false) { Log::error('Failed to parse command line options.'); diff --git a/agent/tests/AgentForwardingTest.php b/agent/tests/AgentForwardingTest.php index f6c8083..f5f8741 100644 --- a/agent/tests/AgentForwardingTest.php +++ b/agent/tests/AgentForwardingTest.php @@ -37,6 +37,7 @@ public function testAgentForwardsEnvelopeToUpstream(): void '"ingest_path":[{"version":"' . str_replace('/', '\/', EnvelopeForwarder::IDENTIFIER . '/' . EnvelopeForwarder::VERSION) . '"}]', $serverOutput['body'] ); + $this->assertStringContainsString('"sentry.php.agent":"true"', $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 6ad6e14..73021f4 100644 --- a/agent/tests/EnvelopeTest.php +++ b/agent/tests/EnvelopeTest.php @@ -53,33 +53,34 @@ public function testCanParseEnvelopeWith2Items(): void $this->assertEquals($payload, (string) $envelope); } - public function testAppendIngestPathToEventItem(): void + public function testPrepareForForwardingAddsIngestPathAndTransportTagToEventItem(): void { $envelope = new Envelope( ['dsn' => 'http://public@example.com/1'], [new EnvelopeItem(['type' => 'event'], '{"message":"test"}')] ); - $envelope->appendIngestPath($this->getIngestPathVersion()); + $envelope->prepareForForwarding($this->getIngestPathVersion()); $payload = json_decode($envelope->getItems()[0]->getData(), true); $this->assertSame([['version' => $this->getIngestPathVersion()]], $payload['ingest_path']); + $this->assertSame("true", $payload['tags'][EnvelopeItem::TRANSPORT_KEY]); } - public function testAppendIngestPathPreservesExistingEntries(): void + public function testPrepareForForwardingPreservesExistingIngestPathAndTags(): 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"}]}' + '{"transaction":"/test","ingest_path":[{"version":"relay/1.0.0","public_key":"abc"}],"tags":{"release_channel":"beta"}}' ), ] ); - $envelope->appendIngestPath($this->getIngestPathVersion()); + $envelope->prepareForForwarding($this->getIngestPathVersion()); $payload = json_decode($envelope->getItems()[0]->getData(), true); @@ -90,58 +91,126 @@ public function testAppendIngestPathPreservesExistingEntries(): void ], $payload['ingest_path'] ); + $this->assertSame( + [ + 'release_channel' => 'beta', + EnvelopeItem::TRANSPORT_KEY => "true", + ], + $payload['tags'] + ); + } + + public function testPrepareForForwardingReplacesInvalidTags(): void + { + $envelope = new Envelope( + ['dsn' => 'http://public@example.com/1'], + [new EnvelopeItem(['type' => 'event'], '{"message":"test","ingest_path":"invalid","tags":"invalid"}')] + ); + + $envelope->prepareForForwarding($this->getIngestPathVersion()); + + $payload = json_decode($envelope->getItems()[0]->getData(), true); + + $this->assertSame([['version' => $this->getIngestPathVersion()]], $payload['ingest_path']); + $this->assertSame([EnvelopeItem::TRANSPORT_KEY => "true"], $payload['tags']); } - public function testAppendIngestPathUpdatesLengthHeader(): void + public function testPrepareForForwardingUpdatesLengthHeader(): void { $envelope = new Envelope( ['dsn' => 'http://public@example.com/1'], [new EnvelopeItem(['type' => 'event', 'length' => 18], '{"message":"test"}')] ); - $envelope->appendIngestPath($this->getIngestPathVersion()); + $envelope->prepareForForwarding($this->getIngestPathVersion()); $item = $envelope->getItems()[0]; $this->assertSame(\strlen($item->getData()), $item->getHeader()['length']); } - public function testAppendIngestPathReplacesInvalidExistingIngestPath(): void + public function testPrepareForForwardingAddsTransportAttributeToLogItems(): void { $envelope = new Envelope( ['dsn' => 'http://public@example.com/1'], - [new EnvelopeItem(['type' => 'event'], '{"message":"test","ingest_path":"invalid"}')] + [ + new EnvelopeItem( + ['type' => 'log'], + '{"items":[{"body":"first","attributes":{"sentry.environment":{"type":"string","value":"production"}}},{"body":"second","attributes":[]}]}' + ), + ] ); - $envelope->appendIngestPath($this->getIngestPathVersion()); + $envelope->prepareForForwarding($this->getIngestPathVersion()); $payload = json_decode($envelope->getItems()[0]->getData(), true); - $this->assertSame([['version' => $this->getIngestPathVersion()]], $payload['ingest_path']); + $this->assertSame('production', $payload['items'][0]['attributes']['sentry.environment']['value']); + $this->assertSame( + ['type' => 'bool', 'value' => true], + $payload['items'][0]['attributes'][EnvelopeItem::TRANSPORT_KEY] + ); + $this->assertSame( + ['type' => 'bool', 'value' => true], + $payload['items'][1]['attributes'][EnvelopeItem::TRANSPORT_KEY] + ); + } + + public function testPrepareForForwardingAddsTransportAttributeToTraceMetricItems(): void + { + $envelope = new Envelope( + ['dsn' => 'http://public@example.com/1'], + [ + new EnvelopeItem( + ['type' => 'trace_metric'], + '{"items":[{"name":"foo","attributes":{"unit":{"type":"string","value":"millisecond"}}}]}' + ), + ] + ); + + $envelope->prepareForForwarding($this->getIngestPathVersion()); + + $payload = json_decode($envelope->getItems()[0]->getData(), true); + + $this->assertSame('millisecond', $payload['items'][0]['attributes']['unit']['value']); + $this->assertSame( + ['type' => 'bool', 'value' => true], + $payload['items'][0]['attributes'][EnvelopeItem::TRANSPORT_KEY] + ); } - public function testAppendIngestPathDoesNotMutateUnsupportedItems(): void + public function testPrepareForForwardingDoesNotMutateUnsupportedItems(): void { - foreach (['log', 'check_in', 'profile', 'attachment'] as $type) { + $payloads = [ + 'check_in' => '{"check_in_id":"abc","monitor_slug":"job","status":"ok"}', + 'profile' => '{"platform":"php","version":"1","client_sdk":{"name":"sentry.php","version":"4.0.0"}}', + 'client_report' => '{"timestamp":1746542641,"discarded_events":[]}', + 'session' => '{"sid":"abc","did":"x","started":"2025-01-01T00:00:00Z","status":"ok"}', + 'replay_event' => '{"platform":"php","sdk":{"name":"sentry.php","version":"4.0.0"},"tags":{"foo":"bar"}}', + 'replay_recording' => '{"items":[{"attributes":{"foo":{"type":"string","value":"bar"}}}]}', + 'feedback' => '{"platform":"php","sdk":{"name":"sentry.php","version":"4.0.0"},"tags":{"foo":"bar"}}', + ]; + + foreach ($payloads as $type => $data) { $envelope = new Envelope( ['dsn' => 'http://public@example.com/1'], - [new EnvelopeItem(['type' => $type], '{"message":"test"}')] + [new EnvelopeItem(['type' => $type], $data)] ); - $envelope->appendIngestPath($this->getIngestPathVersion()); + $envelope->prepareForForwarding($this->getIngestPathVersion()); - $this->assertSame('{"message":"test"}', $envelope->getItems()[0]->getData()); + $this->assertSame($data, $envelope->getItems()[0]->getData(), "Type {$type} should not be mutated"); } } - public function testAppendIngestPathDoesNotMutateMalformedJson(): void + public function testPrepareForForwardingDoesNotMutateMalformedJson(): void { $envelope = new Envelope( ['dsn' => 'http://public@example.com/1'], [new EnvelopeItem(['type' => 'event'], '{"message":')] ); - $envelope->appendIngestPath($this->getIngestPathVersion()); + $envelope->prepareForForwarding($this->getIngestPathVersion()); $this->assertSame('{"message":', $envelope->getItems()[0]->getData()); } diff --git a/bin/sentry-agent b/bin/sentry-agent index 8d8ceb2..f988e2b 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 6c22581..81bf6e9 100644 --- a/bin/sentry-agent.sig +++ b/bin/sentry-agent.sig @@ -1 +1 @@ -E379EBD144ACB3491E2C112C2C4D4A782F3C5C87AE4209E06DE33C6A65315F9D13B83473D397E8773B122C84D80F0DB0E90E0E47C5B7BD3FDCB0A045D703B922 +18A7DD0A2B3B71C36EA79A0793D6EB491DB47F735D3851B22F989FA607DFB927F8D97093DEB66F29AD866C83C2C8FE2F11C1D37309247F9D8747955E67130517