diff --git a/src/sentry/src/Tracing/Aspect/AmqpProducerAspect.php b/src/sentry/src/Tracing/Aspect/AmqpProducerAspect.php index 004505700..6a890607b 100644 --- a/src/sentry/src/Tracing/Aspect/AmqpProducerAspect.php +++ b/src/sentry/src/Tracing/Aspect/AmqpProducerAspect.php @@ -14,7 +14,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Switcher; use FriendsOfHyperf\Sentry\Tracing\SpanStarter; -use FriendsOfHyperf\Sentry\Util\CarrierPacker; +use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\Amqp\Message\ProducerMessage; use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; @@ -33,10 +33,8 @@ class AmqpProducerAspect extends AbstractAspect 'Hyperf\Amqp\Producer::produceMessage', ]; - public function __construct( - protected Switcher $switcher, - protected CarrierPacker $packer - ) { + public function __construct(protected Switcher $switcher) + { } public function process(ProceedingJoinPoint $proceedingJoinPoint) @@ -84,8 +82,7 @@ protected function handleProduceMessage(ProceedingJoinPoint $proceedingJoinPoint 'messaging.amqp.message.exchange' => $producerMessage->getExchange(), 'messaging.amqp.message.pool_name' => $producerMessage->getPoolName(), ]); - - $carrier = $this->packer->pack($span, [ + $carrier = Carrier::fromSpan($span)->with([ 'publish_time' => microtime(true), 'message_id' => $messageId, 'destination_name' => $destinationName, @@ -94,7 +91,7 @@ protected function handleProduceMessage(ProceedingJoinPoint $proceedingJoinPoint (function () use ($carrier) { $this->properties['application_headers'] ??= new AMQPTable(); - $this->properties['application_headers']->set(Constants::TRACE_CARRIER, $carrier); + $this->properties['application_headers']->set(Constants::TRACE_CARRIER, $carrier->toJson()); })->call($producerMessage); return tap($proceedingJoinPoint->process(), fn () => $span->setOrigin('auto.amqp')->finish()); diff --git a/src/sentry/src/Tracing/Aspect/AsyncQueueJobMessageAspect.php b/src/sentry/src/Tracing/Aspect/AsyncQueueJobMessageAspect.php index 69d3836b9..1fc0fe588 100644 --- a/src/sentry/src/Tracing/Aspect/AsyncQueueJobMessageAspect.php +++ b/src/sentry/src/Tracing/Aspect/AsyncQueueJobMessageAspect.php @@ -14,7 +14,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Switcher; use FriendsOfHyperf\Sentry\Tracing\SpanStarter; -use FriendsOfHyperf\Sentry\Util\CarrierPacker; +use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\AsyncQueue\Driver\RedisDriver; use Hyperf\Context\Context; use Hyperf\Di\Aop\AbstractAspect; @@ -25,6 +25,7 @@ /** * @property \Hyperf\AsyncQueue\Driver\ChannelConfig $channel * @property \Hyperf\Redis\RedisProxy $redis + * @property \Hyperf\Contract\PackerInterface $packer * @property string $poolName */ class AsyncQueueJobMessageAspect extends AbstractAspect @@ -39,8 +40,7 @@ class AsyncQueueJobMessageAspect extends AbstractAspect ]; public function __construct( - protected Switcher $switcher, - protected CarrierPacker $packer + protected Switcher $switcher ) { } @@ -98,7 +98,7 @@ public function handlePush(ProceedingJoinPoint $proceedingJoinPoint) }; $span->setData($data); - $carrier = $this->packer->pack($span, [ + $carrier = Carrier::fromSpan($span)->with([ 'publish_time' => microtime(true), 'message_id' => $messageId, 'destination_name' => $destinationName, @@ -137,9 +137,9 @@ protected function handleSerialize(ProceedingJoinPoint $proceedingJoinPoint) return with($proceedingJoinPoint->process(), function ($result) { if (is_array($result) && $carrier = Context::get(Constants::TRACE_CARRIER)) { if (array_is_list($result)) { - $result[] = $carrier; + $result[] = $carrier->toJson(); } elseif (isset($result['job'])) { - $result[Constants::TRACE_CARRIER] = $carrier; + $result[Constants::TRACE_CARRIER] = $carrier->toJson(); } } @@ -151,19 +151,15 @@ protected function handleUnserialize(ProceedingJoinPoint $proceedingJoinPoint) { /** @var array $data */ $data = $proceedingJoinPoint->arguments['keys']['data'] ?? []; - $carrier = null; - - if (is_array($data)) { - if (array_is_list($data)) { - $carrier = array_last($data); - } elseif (isset($data['job'])) { - $carrier = $data[Constants::TRACE_CARRIER] ?? ''; - } - } + $carrier = match (true) { + is_array($data) && array_is_list($data) => array_last($data), + isset($data['job']) => $data[Constants::TRACE_CARRIER] ?? '', + default => null, + }; /** @var string|null $carrier */ if ($carrier) { - Context::set(Constants::TRACE_CARRIER, $carrier); + Context::set(Constants::TRACE_CARRIER, Carrier::fromJson($carrier)); } return $proceedingJoinPoint->process(); diff --git a/src/sentry/src/Tracing/Aspect/KafkaProducerAspect.php b/src/sentry/src/Tracing/Aspect/KafkaProducerAspect.php index 45e71507f..d57de7fb1 100644 --- a/src/sentry/src/Tracing/Aspect/KafkaProducerAspect.php +++ b/src/sentry/src/Tracing/Aspect/KafkaProducerAspect.php @@ -14,7 +14,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Switcher; use FriendsOfHyperf\Sentry\Tracing\SpanStarter; -use FriendsOfHyperf\Sentry\Util\CarrierPacker; +use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; use longlang\phpkafka\Producer\ProduceMessage; @@ -37,8 +37,7 @@ class KafkaProducerAspect extends AbstractAspect ]; public function __construct( - protected Switcher $switcher, - protected CarrierPacker $packer + protected Switcher $switcher ) { } @@ -78,16 +77,17 @@ protected function sendAsync(ProceedingJoinPoint $proceedingJoinPoint) 'messaging.destination.name' => $destinationName, ]); - $carrier = $this->packer->pack($span, [ - 'publish_time' => microtime(true), - 'message_id' => $messageId, - 'destination_name' => $destinationName, - 'body_size' => $bodySize, - ]); + $carrier = Carrier::fromSpan($span) + ->with([ + 'publish_time' => microtime(true), + 'message_id' => $messageId, + 'destination_name' => $destinationName, + 'body_size' => $bodySize, + ]); $headers = $proceedingJoinPoint->arguments['keys']['headers'] ?? []; $headers[] = (new RecordHeader()) ->setHeaderKey(Constants::TRACE_CARRIER) - ->setValue($carrier); + ->setValue($carrier->toJson()); $proceedingJoinPoint->arguments['keys']['headers'] = $headers; return tap($proceedingJoinPoint->process(), fn () => $span->setOrigin('auto.kafka')->finish()); @@ -106,17 +106,16 @@ protected function sendBatchAsync(ProceedingJoinPoint $proceedingJoinPoint) return $proceedingJoinPoint->process(); } - $packer = $this->packer; - foreach ($messages as $message) { - (function () use ($span, $packer) { - $carrier = $packer->pack($span, [ - 'publish_time' => microtime(true), - 'message_id' => uniqid('kafka_', true), - 'destination_name' => $this->getTopic(), - 'body_size' => strlen((string) $this->getValue()), - ]); - $this->headers[] = (new RecordHeader())->setHeaderKey(Constants::TRACE_CARRIER)->setValue($carrier); + (function () use ($span) { + $carrier = Carrier::fromSpan($span) + ->with([ + 'publish_time' => microtime(true), + 'message_id' => uniqid('kafka_', true), + 'destination_name' => $this->getTopic(), + 'body_size' => strlen((string) $this->getValue()), + ]); + $this->headers[] = (new RecordHeader())->setHeaderKey(Constants::TRACE_CARRIER)->setValue($carrier->toJson()); })->call($message); } diff --git a/src/sentry/src/Tracing/Aspect/RpcAspect.php b/src/sentry/src/Tracing/Aspect/RpcAspect.php index 7e8523a73..6e0cc17ba 100644 --- a/src/sentry/src/Tracing/Aspect/RpcAspect.php +++ b/src/sentry/src/Tracing/Aspect/RpcAspect.php @@ -14,7 +14,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Switcher; use FriendsOfHyperf\Sentry\Tracing\SpanStarter; -use FriendsOfHyperf\Sentry\Util\CarrierPacker; +use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\Context\Context; use Hyperf\Contract\ConfigInterface; use Hyperf\Coroutine\Coroutine; @@ -46,8 +46,7 @@ class RpcAspect extends AbstractAspect public function __construct( protected ContainerInterface $container, - protected Switcher $switcher, - protected CarrierPacker $packer + protected Switcher $switcher ) { } @@ -99,7 +98,7 @@ private function handleGenerateRpcPath(ProceedingJoinPoint $proceedingJoinPoint) Context::set(static::SPAN, $span->setData($data)); if ($this->container->has(Rpc\Context::class)) { - $this->container->get(Rpc\Context::class)->set(Constants::TRACE_CARRIER, $this->packer->pack($span)); + $this->container->get(Rpc\Context::class)->set(Constants::TRACE_CARRIER, Carrier::fromSpan($span)->toJson()); } return $path; diff --git a/src/sentry/src/Tracing/Listener/TracingAmqpListener.php b/src/sentry/src/Tracing/Listener/TracingAmqpListener.php index 4472646af..a5da0767e 100644 --- a/src/sentry/src/Tracing/Listener/TracingAmqpListener.php +++ b/src/sentry/src/Tracing/Listener/TracingAmqpListener.php @@ -14,7 +14,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Switcher; use FriendsOfHyperf\Sentry\Tracing\SpanStarter; -use FriendsOfHyperf\Sentry\Util\CarrierPacker; +use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\Amqp\Event\AfterConsume; use Hyperf\Amqp\Event\BeforeConsume; use Hyperf\Amqp\Event\FailToConsume; @@ -32,8 +32,7 @@ class TracingAmqpListener implements ListenerInterface use SpanStarter; public function __construct( - protected Switcher $switcher, - protected CarrierPacker $packer + protected Switcher $switcher ) { } @@ -65,7 +64,7 @@ public function process(object $event): void protected function startTransaction(BeforeConsume $event): void { $message = $event->getMessage(); - $sentryTrace = $baggage = ''; + $carrier = null; if (method_exists($event, 'getAMQPMessage')) { /** @var AMQPMessage $amqpMessage */ @@ -73,14 +72,14 @@ protected function startTransaction(BeforeConsume $event): void /** @var AMQPTable|null $applicationHeaders */ $applicationHeaders = $amqpMessage->has('application_headers') ? $amqpMessage->get('application_headers') : null; if ($applicationHeaders && isset($applicationHeaders[Constants::TRACE_CARRIER])) { - [$sentryTrace, $baggage] = $this->packer->unpack($applicationHeaders[Constants::TRACE_CARRIER]); - Context::set(Constants::TRACE_CARRIER, $applicationHeaders[Constants::TRACE_CARRIER]); + $carrier = Carrier::fromJson($applicationHeaders[Constants::TRACE_CARRIER]); + Context::set(Constants::TRACE_CARRIER, $carrier); } } $this->continueTrace( - sentryTrace: $sentryTrace, - baggage: $baggage, + sentryTrace: $carrier?->getSentryTrace(), + baggage: $carrier?->getBaggage(), name: $message::class, op: 'queue.process', description: $message::class, @@ -97,21 +96,19 @@ protected function finishTransaction(AfterConsume|FailToConsume $event): void return; } - $payload = []; - if ($carrier = Context::get(Constants::TRACE_CARRIER)) { - $payload = json_decode((string) $carrier, true); - } + /** @var Carrier|null $carrier */ + $carrier = Context::get(Constants::TRACE_CARRIER); /** @var ConsumerMessage $message */ $message = $event->getMessage(); $data = [ 'messaging.system' => 'amqp', 'messaging.operation' => 'process', - 'messaging.message.id' => $payload['message_id'] ?? null, - 'messaging.message.body.size' => $payload['body_size'] ?? null, - 'messaging.message.receive.latency' => isset($payload['publish_time']) ? (microtime(true) - $payload['publish_time']) : null, + 'messaging.message.id' => $carrier?->get('message_id'), + 'messaging.message.body.size' => $carrier?->get('body_size'), + 'messaging.message.receive.latency' => $carrier?->has('publish_time') ? (microtime(true) - $carrier->get('publish_time')) : null, 'messaging.message.retry.count' => 0, - 'messaging.destination.name' => $payload['destination_name'] ?? $message->getExchange(), + 'messaging.destination.name' => $carrier?->get('destination_name') ?? $message->getExchange(), // for amqp 'messaging.amqp.message.type' => $message->getTypeString(), 'messaging.amqp.message.routing_key' => $message->getRoutingKey(), diff --git a/src/sentry/src/Tracing/Listener/TracingAsyncQueueListener.php b/src/sentry/src/Tracing/Listener/TracingAsyncQueueListener.php index 2f0af2bff..261987600 100644 --- a/src/sentry/src/Tracing/Listener/TracingAsyncQueueListener.php +++ b/src/sentry/src/Tracing/Listener/TracingAsyncQueueListener.php @@ -14,7 +14,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Switcher; use FriendsOfHyperf\Sentry\Tracing\SpanStarter; -use FriendsOfHyperf\Sentry\Util\CarrierPacker; +use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\AsyncQueue\Event\AfterHandle; use Hyperf\AsyncQueue\Event\BeforeHandle; use Hyperf\AsyncQueue\Event\FailedHandle; @@ -31,8 +31,7 @@ class TracingAsyncQueueListener implements ListenerInterface use SpanStarter; public function __construct( - protected Switcher $switcher, - protected CarrierPacker $packer + protected Switcher $switcher ) { } @@ -64,20 +63,14 @@ public function process(object $event): void protected function startTransaction(BeforeHandle $event): void { - $sentryTrace = $baggage = ''; - - /** @var string|null $carrier */ + /** @var Carrier|null $carrier */ $carrier = Context::get(Constants::TRACE_CARRIER, null, Coroutine::parentId()); - if ($carrier) { - [$sentryTrace, $baggage] = $this->packer->unpack($carrier); - } - $job = $event->getMessage()->job(); $this->continueTrace( - sentryTrace: $sentryTrace, - baggage: $baggage, + sentryTrace: $carrier?->getSentryTrace(), + baggage: $carrier?->getBaggage(), name: $job::class, op: 'queue.process', description: 'async_queue: ' . $job::class, @@ -94,17 +87,16 @@ protected function finishTransaction(AfterHandle|RetryHandle|FailedHandle $event return; } - /** @var string|null $carrier */ + /** @var Carrier|null $carrier */ $carrier = Context::get(Constants::TRACE_CARRIER, null, Coroutine::parentId()); - $payload = json_decode((string) $carrier, true); $data = [ 'messaging.system' => 'async_queue', 'messaging.operation' => 'process', - 'messaging.message.id' => $payload['message_id'] ?? null, - 'messaging.message.body.size' => $payload['body_size'] ?? null, - 'messaging.message.receive.latency' => isset($payload['publish_time']) ? (microtime(true) - $payload['publish_time']) : null, + 'messaging.message.id' => $carrier?->get('message_id'), + 'messaging.message.body.size' => $carrier?->get('body_size'), + 'messaging.message.receive.latency' => $carrier?->has('publish_time') ? (microtime(true) - $carrier->get('publish_time')) : null, 'messaging.message.retry.count' => $event->getMessage()->getAttempts(), - 'messaging.destination.name' => $payload['destination_name'] ?? null, + 'messaging.destination.name' => $carrier?->get('destination_name'), ]; $tags = []; diff --git a/src/sentry/src/Tracing/Listener/TracingKafkaListener.php b/src/sentry/src/Tracing/Listener/TracingKafkaListener.php index 316d16500..13db955fd 100644 --- a/src/sentry/src/Tracing/Listener/TracingKafkaListener.php +++ b/src/sentry/src/Tracing/Listener/TracingKafkaListener.php @@ -14,7 +14,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Switcher; use FriendsOfHyperf\Sentry\Tracing\SpanStarter; -use FriendsOfHyperf\Sentry\Util\CarrierPacker; +use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\Context\Context; use Hyperf\Event\Contract\ListenerInterface; use Hyperf\Kafka\Event\AfterConsume; @@ -30,8 +30,7 @@ class TracingKafkaListener implements ListenerInterface use SpanStarter; public function __construct( - protected Switcher $switcher, - protected CarrierPacker $packer + protected Switcher $switcher ) { } @@ -64,21 +63,21 @@ protected function startTransaction(BeforeConsume $event): void { $consumer = $event->getConsumer(); $message = $event->getData(); - $sentryTrace = $baggage = ''; + $carrier = null; if ($message instanceof ConsumeMessage) { foreach ($message->getHeaders() as $header) { if ($header->getHeaderKey() === Constants::TRACE_CARRIER) { - [$sentryTrace, $baggage] = $this->packer->unpack($header->getValue()); - Context::set(Constants::TRACE_CARRIER, $header->getValue()); + $carrier = Carrier::fromJson($header->getValue()); + Context::set(Constants::TRACE_CARRIER, $carrier); break; } } } $this->continueTrace( - sentryTrace: $sentryTrace, - baggage: $baggage, + sentryTrace: $carrier?->getSentryTrace() ?? '', + baggage: $carrier?->getBaggage() ?? '', name: $consumer->getTopic() . ' process', op: 'queue.process', description: $consumer::class, @@ -95,21 +94,18 @@ protected function finishTransaction(AfterConsume|FailToConsume $event): void return; } - $payload = []; - if ($carrier = (string) Context::get(Constants::TRACE_CARRIER)) { - $payload = json_decode($carrier, true); - } - + /** @var Carrier|null $carrier */ + $carrier = Context::get(Constants::TRACE_CARRIER); $consumer = $event->getConsumer(); $tags = []; $data = [ 'messaging.system' => 'kafka', 'messaging.operation' => 'process', - 'messaging.message.id' => $payload['message_id'] ?? null, - 'messaging.message.body.size' => $payload['body_size'] ?? null, - 'messaging.message.receive.latency' => isset($payload['publish_time']) ? (microtime(true) - $payload['publish_time']) : null, + 'messaging.message.id' => $carrier?->get('message_id'), + 'messaging.message.body.size' => $carrier?->get('body_size'), + 'messaging.message.receive.latency' => $carrier?->has('publish_time') ? (microtime(true) - $carrier->get('publish_time')) : null, 'messaging.message.retry.count' => 0, - 'messaging.destination.name' => $payload['destination_name'] ?? 'unknown', + 'messaging.destination.name' => $carrier?->get('destination_name'), // for kafka 'messaging.kafka.consumer.group' => $consumer->getGroupId(), 'messaging.kafka.consumer.pool' => $consumer->getPool(), diff --git a/src/sentry/src/Tracing/SpanStarter.php b/src/sentry/src/Tracing/SpanStarter.php index 460617537..05d48d603 100644 --- a/src/sentry/src/Tracing/SpanStarter.php +++ b/src/sentry/src/Tracing/SpanStarter.php @@ -12,7 +12,7 @@ namespace FriendsOfHyperf\Sentry\Tracing; use FriendsOfHyperf\Sentry\Constants; -use FriendsOfHyperf\Sentry\Util\CarrierPacker; +use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\Context\ApplicationContext; use Hyperf\Rpc\Context as RpcContext; use Psr\Http\Message\ServerRequestInterface; @@ -59,12 +59,14 @@ protected function startRequestTransaction(ServerRequestInterface $request, ...$ $baggage = $request->getHeaderLine('baggage'); $container = $this->container ?? ApplicationContext::getContainer(); + // Rpc Context if ($container->has(RpcContext::class)) { $rpcContext = $container->get(RpcContext::class); - $carrier = $rpcContext->get(Constants::TRACE_CARRIER); - if ($carrier) { - $packer = $container->get(CarrierPacker::class); - [$sentryTrace, $baggage] = $packer->unpack($carrier); + /** @var string|null $payload */ + $payload = $rpcContext->get(Constants::TRACE_CARRIER); + if ($payload) { + $carrier = Carrier::fromJson($payload); + [$sentryTrace, $baggage] = [$carrier->getSentryTrace(), $carrier->getBaggage()]; } } diff --git a/src/sentry/src/Util/Carrier.php b/src/sentry/src/Util/Carrier.php new file mode 100644 index 000000000..3b39c7167 --- /dev/null +++ b/src/sentry/src/Util/Carrier.php @@ -0,0 +1,111 @@ +toJson(); + } + + public static function fromArray(array $data): static + { + return new static($data); + } + + public static function fromJson(string $json): static + { + try { + $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + if (! is_array($data)) { + $data = []; + } + } catch (JsonException $e) { + $data = []; + } + + return new static($data); + } + + public static function fromSpan(Span $span): static + { + return new static([ + 'sentry-trace' => $span->toTraceparent(), + 'baggage' => $span->toBaggage(), + 'traceparent' => $span->toW3CTraceparent(), + ]); + } + + public function with(array $data): static + { + $new = clone $this; + $new->data = array_merge($this->data, $data); + return $new; + } + + public function getSentryTrace(): string + { + return $this->data['sentry-trace'] ?? ''; + } + + public function getBaggage(): string + { + return $this->data['baggage'] ?? ''; + } + + public function getTraceparent(): string + { + return $this->data['traceparent'] ?? ''; + } + + public function has(string $key): bool + { + return isset($this->data[$key]); + } + + public function get(string $key, mixed $default = null): mixed + { + return $this->data[$key] ?? $default; + } + + public function jsonSerialize(): mixed + { + return $this->data; + } + + public function toArray(): array + { + return $this->jsonSerialize(); + } + + public function toJson(int $options = JSON_UNESCAPED_UNICODE): string + { + try { + return json_encode($this->toArray(), $options | JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + return '{}'; + } + } +} diff --git a/src/sentry/src/Util/CarrierPacker.php b/src/sentry/src/Util/CarrierPacker.php index 5eb8fbe98..99e0a3af7 100644 --- a/src/sentry/src/Util/CarrierPacker.php +++ b/src/sentry/src/Util/CarrierPacker.php @@ -14,6 +14,9 @@ use Sentry\Tracing\Span; use Throwable; +/** + * @deprecated since v3.1, use FriendsOfHyperf\Sentry\Util\Carrier instead, will be removed in v3.2 + */ class CarrierPacker { /** diff --git a/tests/Sentry/CarrierTest.php b/tests/Sentry/CarrierTest.php new file mode 100644 index 000000000..5fe0f9bfe --- /dev/null +++ b/tests/Sentry/CarrierTest.php @@ -0,0 +1,298 @@ +testData = [ + 'sentry-trace' => '12345678901234567890123456789012-1234567890123456-1', + 'baggage' => 'sentry-trace_id=12345678901234567890123456789012,sentry-public_key=public_key', + 'traceparent' => '00-12345678901234567890123456789012-1234567890123456-01', + 'custom_field' => 'custom_value', + 'publish_time' => 1234567890.123, + ]; +}); + +describe('Carrier Construction', function () { + test('can be constructed with empty data', function () { + $carrier = new Carrier(); + expect($carrier->toArray())->toBe([]); + }); + + test('can be constructed with initial data', function () { + $carrier = new Carrier($this->testData); + expect($carrier->toArray())->toBe($this->testData); + }); + + test('can be created from array', function () { + $carrier = Carrier::fromArray($this->testData); + expect($carrier->toArray())->toBe($this->testData); + }); + + test('can be created from valid JSON', function () { + $json = json_encode($this->testData); + $carrier = Carrier::fromJson($json); + expect($carrier->toArray())->toBe($this->testData); + }); + + test('handles invalid JSON gracefully', function () { + $carrier = Carrier::fromJson('invalid json'); + expect($carrier->toArray())->toBe([]); + }); + + test('handles non-array JSON gracefully', function () { + $carrier = Carrier::fromJson('"string value"'); + expect($carrier->toArray())->toBe([]); + }); + + test('handles empty JSON', function () { + $carrier = Carrier::fromJson(''); + expect($carrier->toArray())->toBe([]); + }); + + test('can be created from Span', function () { + $spanContext = new SpanContext(); + $span = new Span($spanContext); + + $carrier = Carrier::fromSpan($span); + $data = $carrier->toArray(); + + expect($data)->toHaveKeys(['sentry-trace', 'baggage', 'traceparent']); + expect($data['sentry-trace'])->toBeString(); + expect($data['baggage'])->toBeString(); + expect($data['traceparent'])->toBeString(); + }); +}); + +describe('Data Manipulation', function () { + test('can add data using with method', function () { + $carrier = new Carrier(['existing' => 'value']); + $newCarrier = $carrier->with(['new' => 'data']); + + expect($carrier->toArray())->toBe(['existing' => 'value']); + expect($newCarrier->toArray())->toBe(['existing' => 'value', 'new' => 'data']); + }); + + test('with method merges data correctly', function () { + $carrier = new Carrier(['key1' => 'value1', 'key2' => 'value2']); + $newCarrier = $carrier->with(['key2' => 'updated', 'key3' => 'new']); + + expect($newCarrier->toArray())->toBe([ + 'key1' => 'value1', + 'key2' => 'updated', + 'key3' => 'new', + ]); + }); +}); + +describe('Data Access', function () { + test('can check if key exists', function () { + $carrier = new Carrier($this->testData); + + expect($carrier->has('sentry-trace'))->toBeTrue(); + expect($carrier->has('nonexistent'))->toBeFalse(); + }); + + test('can get value by key', function () { + $carrier = new Carrier($this->testData); + + expect($carrier->get('custom_field'))->toBe('custom_value'); + expect($carrier->get('nonexistent'))->toBeNull(); + expect($carrier->get('nonexistent', 'default'))->toBe('default'); + }); + + test('getSentryTrace returns correct value', function () { + $carrier = new Carrier($this->testData); + expect($carrier->getSentryTrace())->toBe('12345678901234567890123456789012-1234567890123456-1'); + }); + + test('getSentryTrace returns empty string when not set', function () { + $carrier = new Carrier(); + expect($carrier->getSentryTrace())->toBe(''); + }); + + test('getBaggage returns correct value', function () { + $carrier = new Carrier($this->testData); + expect($carrier->getBaggage())->toBe('sentry-trace_id=12345678901234567890123456789012,sentry-public_key=public_key'); + }); + + test('getBaggage returns empty string when not set', function () { + $carrier = new Carrier(); + expect($carrier->getBaggage())->toBe(''); + }); + + test('getTraceparent returns correct value', function () { + $carrier = new Carrier($this->testData); + expect($carrier->getTraceparent())->toBe('00-12345678901234567890123456789012-1234567890123456-01'); + }); + + test('getTraceparent returns empty string when not set', function () { + $carrier = new Carrier(); + expect($carrier->getTraceparent())->toBe(''); + }); +}); + +describe('Serialization', function () { + test('implements JsonSerializable', function () { + $carrier = new Carrier($this->testData); + expect($carrier->jsonSerialize())->toBe($this->testData); + }); + + test('toArray returns correct data', function () { + $carrier = new Carrier($this->testData); + expect($carrier->toArray())->toBe($this->testData); + }); + + test('toJson returns valid JSON string', function () { + $carrier = new Carrier($this->testData); + $json = $carrier->toJson(); + + expect($json)->toBeString(); + expect(json_decode($json, true))->toBe($this->testData); + }); + + test('toJson with custom options', function () { + $data = ['key' => 'value with unicode: 中文']; + $carrier = new Carrier($data); + $json = $carrier->toJson(JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + + expect($json)->toContain('中文'); + expect($json)->toContain("\n"); // Pretty print adds newlines + }); + + test('toJson handles encoding errors gracefully', function () { + // Create data that cannot be JSON encoded + $resource = fopen('php://memory', 'r'); + $carrier = new Carrier(['resource' => $resource]); + fclose($resource); + + $json = $carrier->toJson(); + expect($json)->toBe('{}'); + }); + + test('__toString returns JSON representation', function () { + $carrier = new Carrier($this->testData); + $string = (string) $carrier; + + expect($string)->toBe($carrier->toJson()); + expect(json_decode($string, true))->toBe($this->testData); + }); + + test('can be JSON encoded directly', function () { + $carrier = new Carrier($this->testData); + $json = json_encode($carrier); + + expect(json_decode($json, true))->toBe($this->testData); + }); +}); + +describe('Edge Cases and Error Handling', function () { + test('handles null values in data', function () { + $data = ['key' => null, 'other' => 'value']; + $carrier = new Carrier($data); + + expect($carrier->get('key'))->toBeNull(); + expect($carrier->has('key'))->toBeFalse(); // isset() returns false for null values + expect($carrier->toArray())->toBe($data); + }); + + test('handles boolean and numeric values', function () { + $data = [ + 'bool_true' => true, + 'bool_false' => false, + 'int' => 42, + 'float' => 3.14, + ]; + $carrier = new Carrier($data); + + expect($carrier->get('bool_true'))->toBeTrue(); + expect($carrier->get('bool_false'))->toBeFalse(); + expect($carrier->get('int'))->toBe(42); + expect($carrier->get('float'))->toBe(3.14); + }); + + test('handles array and object values', function () { + $data = [ + 'array' => ['nested', 'array'], + 'object' => (object) ['property' => 'value'], + ]; + $carrier = new Carrier($data); + + expect($carrier->get('array'))->toBe(['nested', 'array']); + expect($carrier->get('object'))->toBeInstanceOf('stdClass'); + }); + + test('immutability of original carrier in with method', function () { + $original = new Carrier(['original' => 'data']); + $modified = $original->with(['new' => 'data']); + + expect($original === $modified)->toBeFalse(); + expect($original->has('new'))->toBeFalse(); + expect($modified->has('new'))->toBeTrue(); + }); + + test('deep cloning behavior', function () { + $data = ['nested' => ['array' => 'value']]; + $original = new Carrier($data); + $modified = $original->with(['other' => 'data']); + + // Modify the nested array in the modified carrier + $modifiedData = $modified->toArray(); + $modifiedData['nested']['array'] = 'changed'; + + // Original should remain unchanged + expect($original->get('nested'))->toBe(['array' => 'value']); + }); +}); + +describe('Integration Tests', function () { + test('round trip JSON serialization', function () { + $original = new Carrier($this->testData); + $json = $original->toJson(); + $restored = Carrier::fromJson($json); + + expect($restored->toArray())->toBe($original->toArray()); + }); + + test('chaining operations', function () { + $carrier = (new Carrier()) + ->with(['step1' => 'data1']) + ->with(['step2' => 'data2']) + ->with(['step1' => 'updated']); // Override step1 + + expect($carrier->toArray())->toBe([ + 'step1' => 'updated', + 'step2' => 'data2', + ]); + }); + + test('works with Span integration', function () { + $spanContext = new SpanContext(); + $span = new Span($spanContext); + + $carrier = Carrier::fromSpan($span) + ->with(['custom' => 'data']); + + $data = $carrier->toArray(); + expect($data)->toHaveKey('custom'); + expect($data)->toHaveKeys(['sentry-trace', 'baggage', 'traceparent']); + + // Test round trip + $json = $carrier->toJson(); + $restored = Carrier::fromJson($json); + expect($restored->getSentryTrace())->toBe($carrier->getSentryTrace()); + expect($restored->get('custom'))->toBe('data'); + }); +});