Skip to content
Merged
4 changes: 2 additions & 2 deletions agent/src/Envelope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion agent/src/EnvelopeForwarder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
73 changes: 56 additions & 17 deletions agent/src/EnvelopeItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<array-key, mixed> $payload */
$payload = JSON::decode($this->data);
$payload = self::addIngestPathAndTransportTag($payload, $client);
break;
case 'log':
case 'trace_metric':
/** @var array<array-key, mixed> $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<array-key, mixed> $payload
*
* @return array<array-key, mixed>
*/
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<array-key, mixed> $payload
*
* @return array<array-key, mixed>
*/
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()
Expand Down
2 changes: 1 addition & 1 deletion agent/src/sentry-agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
1 change: 1 addition & 0 deletions agent/tests/AgentForwardingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down
105 changes: 87 additions & 18 deletions agent/tests/EnvelopeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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());
}
Expand Down
Binary file modified bin/sentry-agent
Binary file not shown.
2 changes: 1 addition & 1 deletion bin/sentry-agent.sig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
E379EBD144ACB3491E2C112C2C4D4A782F3C5C87AE4209E06DE33C6A65315F9D13B83473D397E8773B122C84D80F0DB0E90E0E47C5B7BD3FDCB0A045D703B922
18A7DD0A2B3B71C36EA79A0793D6EB491DB47F735D3851B22F989FA607DFB927F8D97093DEB66F29AD866C83C2C8FE2F11C1D37309247F9D8747955E67130517
Loading