From c32efeebe9b353ec5c939c33124dcd8305c0a586 Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Fri, 20 Feb 2026 10:56:28 +0100 Subject: [PATCH 1/2] feat: add missing ingestion parameters from Langfuse API --- README.md | 11 +- src/Ingestion.php | 26 +++ src/Ingestion/Generation.php | 14 ++ src/Ingestion/Span.php | 26 +++ src/Ingestion/Trace.php | 26 +++ src/Prompt.php | 15 +- tests/Feature/IngestionTest.php | 337 +++++++++++++++++--------------- 7 files changed, 289 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 05190e9..8cbc0b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ## Langfuse PHP - A PHP Client for Langfuse API +> **For detailed documentation, examples, and parameter references, see the [Wiki](../../wiki).** + This package provides a wrapper around the [Langfuse](https://langfuse.com) API, allowing you to easily integrate Langfuse into your PHP applications. It uses as few dependencies as possible. ### This package supports the following features: @@ -134,6 +136,11 @@ $gen = $trace->generation( modelParameters: ['temperature' => 0.7], promptName: 'my-prompt', promptVersion: 1, + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-01-01T00:00:05Z', + completionStartTime: '2025-01-01T00:00:03Z', + usageDetails: ['input' => 10, 'output' => 20, 'total' => 30], + costDetails: ['input' => 0.001, 'output' => 0.002, 'total' => 0.003], ); // Generation nested under a span @@ -147,7 +154,9 @@ $gen = $span->generation( // Update a generation after the LLM responds $gen->update( output: 'updated response', - metadata: ['tokens' => 150], + endTime: date('c'), + usageDetails: ['input' => 15, 'output' => 25, 'total' => 40], + costDetails: ['total' => 0.004], ); ``` diff --git a/src/Ingestion.php b/src/Ingestion.php index d581c87..c5e4cfe 100644 --- a/src/Ingestion.php +++ b/src/Ingestion.php @@ -50,6 +50,9 @@ public function trace( array|string|null $output = null, ?array $metadata = null, ?array $tags = null, + ?string $release = null, + ?string $version = null, + ?bool $public = null, ): Trace { $traceId ??= self::uuid(); @@ -63,6 +66,9 @@ public function trace( 'output' => $output, 'metadata' => $metadata, 'tags' => $tags, + 'release' => $release, + 'version' => $version, + 'public' => $public, 'environment' => $this->environment, ], static fn (mixed $v): bool => $v !== null); @@ -91,6 +97,9 @@ public function span( ?string $startTime = null, ?string $endTime = null, ?array $metadata = null, + ?string $level = null, + ?string $statusMessage = null, + ?string $version = null, ): Span { $spanId ??= self::uuid(); @@ -104,6 +113,9 @@ public function span( 'input' => $input, 'output' => $output, 'metadata' => $metadata, + 'level' => $level, + 'statusMessage' => $statusMessage, + 'version' => $version, 'environment' => $this->environment, ], static fn (mixed $v): bool => $v !== null); @@ -123,6 +135,8 @@ public function span( * @param array|string $output * @param array|null $modelParameters * @param array|null $metadata + * @param array|null $usageDetails + * @param array|null $costDetails */ public function generation( string $traceId, @@ -138,6 +152,12 @@ public function generation( ?array $metadata = null, ?string $startTime = null, ?string $endTime = null, + ?string $completionStartTime = null, + ?array $usageDetails = null, + ?array $costDetails = null, + ?string $level = null, + ?string $statusMessage = null, + ?string $version = null, ): Generation { $generationId ??= self::uuid(); @@ -148,6 +168,7 @@ public function generation( 'name' => $name, 'startTime' => $startTime ?? self::now(), 'endTime' => $endTime, + 'completionStartTime' => $completionStartTime, 'input' => $input, 'output' => $output, 'model' => $model, @@ -155,6 +176,11 @@ public function generation( 'promptName' => $promptName, 'promptVersion' => $promptVersion, 'metadata' => $metadata, + 'usageDetails' => $usageDetails, + 'costDetails' => $costDetails, + 'level' => $level, + 'statusMessage' => $statusMessage, + 'version' => $version, 'environment' => $this->environment, ], static fn (mixed $v): bool => $v !== null); diff --git a/src/Ingestion/Generation.php b/src/Ingestion/Generation.php index 13350cb..e1d936e 100644 --- a/src/Ingestion/Generation.php +++ b/src/Ingestion/Generation.php @@ -26,6 +26,8 @@ public function __construct( * @param array|string|null $output * @param array|null $modelParameters * @param array|null $metadata + * @param array|null $usageDetails + * @param array|null $costDetails */ public function update( array|string|null $input = null, @@ -34,6 +36,12 @@ public function update( ?array $modelParameters = null, ?array $metadata = null, ?string $endTime = null, + ?string $completionStartTime = null, + ?array $usageDetails = null, + ?array $costDetails = null, + ?string $level = null, + ?string $statusMessage = null, + ?string $version = null, ): self { $body = array_filter([ 'id' => $this->generationId, @@ -44,6 +52,12 @@ public function update( 'modelParameters' => $modelParameters, 'metadata' => $metadata, 'endTime' => $endTime, + 'completionStartTime' => $completionStartTime, + 'usageDetails' => $usageDetails, + 'costDetails' => $costDetails, + 'level' => $level, + 'statusMessage' => $statusMessage, + 'version' => $version, ], static fn (mixed $v): bool => $v !== null); $this->ingestion->send('generation-update', $body); diff --git a/src/Ingestion/Span.php b/src/Ingestion/Span.php index 65080e3..ed9af6f 100644 --- a/src/Ingestion/Span.php +++ b/src/Ingestion/Span.php @@ -32,6 +32,9 @@ public function update( array|string|null $output = null, ?string $endTime = null, ?array $metadata = null, + ?string $level = null, + ?string $statusMessage = null, + ?string $version = null, ): self { $body = array_filter([ 'id' => $this->spanId, @@ -41,6 +44,9 @@ public function update( 'output' => $output, 'endTime' => $endTime, 'metadata' => $metadata, + 'level' => $level, + 'statusMessage' => $statusMessage, + 'version' => $version, ], static fn (mixed $v): bool => $v !== null); $this->ingestion->send('span-update', $body); @@ -63,6 +69,9 @@ public function span( ?string $startTime = null, ?string $endTime = null, ?array $metadata = null, + ?string $level = null, + ?string $statusMessage = null, + ?string $version = null, ): self { return $this->ingestion->span( traceId: $this->traceId, @@ -74,6 +83,9 @@ public function span( startTime: $startTime, endTime: $endTime, metadata: $metadata, + level: $level, + statusMessage: $statusMessage, + version: $version, ); } @@ -84,6 +96,8 @@ public function span( * @param array|string $output * @param array|null $modelParameters * @param array|null $metadata + * @param array|null $usageDetails + * @param array|null $costDetails */ public function generation( string $name, @@ -97,6 +111,12 @@ public function generation( ?array $metadata = null, ?string $startTime = null, ?string $endTime = null, + ?string $completionStartTime = null, + ?array $usageDetails = null, + ?array $costDetails = null, + ?string $level = null, + ?string $statusMessage = null, + ?string $version = null, ): Generation { return $this->ingestion->generation( traceId: $this->traceId, @@ -112,6 +132,12 @@ public function generation( metadata: $metadata, startTime: $startTime, endTime: $endTime, + completionStartTime: $completionStartTime, + usageDetails: $usageDetails, + costDetails: $costDetails, + level: $level, + statusMessage: $statusMessage, + version: $version, ); } } diff --git a/src/Ingestion/Trace.php b/src/Ingestion/Trace.php index ae7535d..cf7693d 100644 --- a/src/Ingestion/Trace.php +++ b/src/Ingestion/Trace.php @@ -34,6 +34,9 @@ public function update( array|string|null $output = null, ?array $metadata = null, ?array $tags = null, + ?string $release = null, + ?string $version = null, + ?bool $public = null, ): self { $body = array_filter([ 'id' => $this->traceId, @@ -44,6 +47,9 @@ public function update( 'output' => $output, 'metadata' => $metadata, 'tags' => $tags, + 'release' => $release, + 'version' => $version, + 'public' => $public, ], static fn (mixed $v): bool => $v !== null); $this->ingestion->send('trace-create', $body); @@ -66,6 +72,9 @@ public function span( ?string $startTime = null, ?string $endTime = null, ?array $metadata = null, + ?string $level = null, + ?string $statusMessage = null, + ?string $version = null, ): Span { return $this->ingestion->span( traceId: $this->traceId, @@ -76,6 +85,9 @@ public function span( startTime: $startTime, endTime: $endTime, metadata: $metadata, + level: $level, + statusMessage: $statusMessage, + version: $version, ); } @@ -86,6 +98,8 @@ public function span( * @param array|string $output * @param array|null $modelParameters * @param array|null $metadata + * @param array|null $usageDetails + * @param array|null $costDetails */ public function generation( string $name, @@ -99,6 +113,12 @@ public function generation( ?array $metadata = null, ?string $startTime = null, ?string $endTime = null, + ?string $completionStartTime = null, + ?array $usageDetails = null, + ?array $costDetails = null, + ?string $level = null, + ?string $statusMessage = null, + ?string $version = null, ): Generation { return $this->ingestion->generation( traceId: $this->traceId, @@ -113,6 +133,12 @@ public function generation( metadata: $metadata, startTime: $startTime, endTime: $endTime, + completionStartTime: $completionStartTime, + usageDetails: $usageDetails, + costDetails: $costDetails, + level: $level, + statusMessage: $statusMessage, + version: $version, ); } } diff --git a/src/Prompt.php b/src/Prompt.php index 42c6e4b..3edee52 100644 --- a/src/Prompt.php +++ b/src/Prompt.php @@ -21,7 +21,8 @@ class Prompt public function __construct( private readonly TransporterInterface $transporter, private readonly string $defaultLabel, - ) {} + ) { + } /** * Retrieve a text prompt by name. Uses default label if no version or label provided. @@ -49,7 +50,7 @@ public function text(string $promptName, ?int $version = null, ?string $label = /** * Retrieve a chat prompt by name. Uses default label if no version or label provided. * - * @param array|null $fallback + * @param array|null $fallback * * @throws InvalidPromptTypeException */ @@ -96,10 +97,10 @@ public function list(?string $name = null, ?int $version = null, ?string $label /** * Create a new prompt. * - * @param ($type is PromptType::TEXT ? string : array) $prompt - * @param array|null $labels - * @param array|null $config - * @param array|null $tags + * @param ($type is PromptType::TEXT ? string : array) $prompt + * @param array|null $labels + * @param array|null $config + * @param array|null $tags * @return ($type is PromptType::TEXT ? TextPromptResponse : ChatPromptResponse) * * @throws JsonException @@ -164,7 +165,7 @@ public function create(string $promptName, string|array $prompt, PromptType $typ /** * Update labels for a specific prompt version. * - * @param array $labels + * @param array $labels * * @throws JsonException */ diff --git a/tests/Feature/IngestionTest.php b/tests/Feature/IngestionTest.php index e61ca1a..e803d39 100644 --- a/tests/Feature/IngestionTest.php +++ b/tests/Feature/IngestionTest.php @@ -77,7 +77,7 @@ function getEventType(array $history, int $index = 0): string // ─── Trace ────────────────────────────────────────────────────────────────── -it('creates a trace and posts to ingestion endpoint', function (): void { +it('creates a trace with all parameters', function (): void { // Arrange $history = []; $ingestion = makeIngestion($history); @@ -86,59 +86,42 @@ function getEventType(array $history, int $index = 0): string $trace = $ingestion->trace( name: 'test-trace', traceId: 'my-trace-id', + sessionId: 'sess-456', + userId: 'user-123', input: 'hello', + output: 'world', + metadata: ['source' => 'test'], + tags: ['tag1', 'tag2'], + release: '1.2.3', + version: '2.0.0', + public: true, ); // Assert - expect($trace)->toBeInstanceOf(Trace::class) - ->and($trace->id)->toBe('my-trace-id') - ->and($history)->toHaveCount(1); - /** @var RequestInterface $request */ $request = $history[0]['request']; - expect($request->getMethod())->toBe('POST') - ->and((string) $request->getUri())->toContain('/api/public/ingestion'); - $body = getEventBody($history); - expect(getEventType($history))->toBe('trace-create') + + expect($trace)->toBeInstanceOf(Trace::class) + ->and($trace->id)->toBe('my-trace-id') + ->and($history)->toHaveCount(1) + ->and($request->getMethod())->toBe('POST') + ->and((string) $request->getUri())->toContain('/api/public/ingestion') + ->and(getEventType($history))->toBe('trace-create') ->and($body['id'])->toBe('my-trace-id') ->and($body['name'])->toBe('test-trace') + ->and($body['userId'])->toBe('user-123') + ->and($body['sessionId'])->toBe('sess-456') ->and($body['input'])->toBe('hello') + ->and($body['output'])->toBe('world') + ->and($body['metadata'])->toBe(['source' => 'test']) + ->and($body['tags'])->toBe(['tag1', 'tag2']) + ->and($body['release'])->toBe('1.2.3') + ->and($body['version'])->toBe('2.0.0') + ->and($body['public'])->toBeTrue() ->and($body['environment'])->toBe('testing'); }); -it('creates a trace with userId and sessionId', function (): void { - // Arrange - $history = []; - $ingestion = makeIngestion($history); - - // Act - $ingestion->trace(name: 'test-trace', userId: 'user-123', sessionId: 'sess-456'); - - // Assert - $body = getEventBody($history); - expect($body['userId'])->toBe('user-123') - ->and($body['sessionId'])->toBe('sess-456'); -}); - -it('creates a trace with metadata and tags', function (): void { - // Arrange - $history = []; - $ingestion = makeIngestion($history); - - // Act - $ingestion->trace( - name: 'test-trace', - metadata: ['source' => 'test'], - tags: ['tag1', 'tag2'], - ); - - // Assert - $body = getEventBody($history); - expect($body['metadata'])->toBe(['source' => 'test']) - ->and($body['tags'])->toBe(['tag1', 'tag2']); -}); - it('omits null values from trace body', function (): void { // Arrange $history = []; @@ -154,35 +137,43 @@ function getEventType(array $history, int $index = 0): string ->and($body)->not->toHaveKey('input') ->and($body)->not->toHaveKey('output') ->and($body)->not->toHaveKey('metadata') - ->and($body)->not->toHaveKey('tags'); + ->and($body)->not->toHaveKey('tags') + ->and($body)->not->toHaveKey('release') + ->and($body)->not->toHaveKey('version') + ->and($body)->not->toHaveKey('public'); }); -// ─── Trace update ─────────────────────────────────────────────────────────── - -it('updates a trace with a second POST', function (): void { +it('updates a trace with all parameters', function (): void { // Arrange $history = []; $ingestion = makeIngestion($history); // Act - $trace = $ingestion->trace(name: 'my-trace', input: 'start'); - $result = $trace->update(output: 'final result', userId: 'user-456'); + $trace = $ingestion->trace(name: 'my-trace'); + $result = $trace->update( + output: 'final result', + userId: 'user-456', + release: '1.0.0', + version: '3.0.0', + public: false, + ); // Assert - expect($result)->toBe($trace) - ->and($history)->toHaveCount(2); - - expect(getEventType($history, index: 1))->toBe('trace-create'); - $body = getEventBody($history, index: 1); - expect($body['id'])->toBe($trace->id) + expect($result)->toBe($trace) + ->and($history)->toHaveCount(2) + ->and(getEventType($history, index: 1))->toBe('trace-create') + ->and($body['id'])->toBe($trace->id) ->and($body['output'])->toBe('final result') - ->and($body['userId'])->toBe('user-456'); + ->and($body['userId'])->toBe('user-456') + ->and($body['release'])->toBe('1.0.0') + ->and($body['version'])->toBe('3.0.0') + ->and($body['public'])->toBeFalse(); }); // ─── Span ─────────────────────────────────────────────────────────────────── -it('creates a span and returns a Span', function (): void { +it('creates a span with all parameters', function (): void { // Arrange $history = []; $ingestion = makeIngestion($history); @@ -191,64 +182,69 @@ function getEventType(array $history, int $index = 0): string $span = $ingestion->span( traceId: 'my-trace-id', name: 'web-search-batch', + spanId: 'custom-span-id', + parentObservationId: 'parent-123', input: ['query' => 'test'], + output: 'result', + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-01-01T00:00:02Z', + metadata: ['source' => 'test'], + level: 'WARNING', + statusMessage: 'Something went wrong', + version: '1.0.0', ); // Assert - expect($span)->toBeInstanceOf(Span::class) - ->and($span->id)->toBeString() - ->and($history)->toHaveCount(1); - $body = getEventBody($history); - expect(getEventType($history))->toBe('span-create') + expect($span)->toBeInstanceOf(Span::class) + ->and($span->id)->toBe('custom-span-id') + ->and($history)->toHaveCount(1) + ->and(getEventType($history))->toBe('span-create') + ->and($body['id'])->toBe('custom-span-id') ->and($body['traceId'])->toBe('my-trace-id') + ->and($body['parentObservationId'])->toBe('parent-123') ->and($body['name'])->toBe('web-search-batch') - ->and($body['input'])->toBe(['query' => 'test']); -}); - -it('creates a span with a provided spanId', function (): void { - // Arrange - $history = []; - $ingestion = makeIngestion($history); - - // Act - $span = $ingestion->span( - traceId: 'my-trace-id', - name: 'my-span', - spanId: 'custom-span-id', - ); - - // Assert - expect($span->id)->toBe('custom-span-id'); - - $body = getEventBody($history); - expect($body['id'])->toBe('custom-span-id'); + ->and($body['input'])->toBe(['query' => 'test']) + ->and($body['output'])->toBe('result') + ->and($body['startTime'])->toBe('2025-01-01T00:00:00Z') + ->and($body['endTime'])->toBe('2025-01-01T00:00:02Z') + ->and($body['metadata'])->toBe(['source' => 'test']) + ->and($body['level'])->toBe('WARNING') + ->and($body['statusMessage'])->toBe('Something went wrong') + ->and($body['version'])->toBe('1.0.0'); }); -it('updates a span with span-update type', function (): void { +it('updates a span with all parameters', function (): void { // Arrange $history = []; $ingestion = makeIngestion($history); // Act $span = $ingestion->span(traceId: 'my-trace-id', name: 'search-span'); - $result = $span->update(output: ['results' => 3], endTime: '2025-06-01T12:00:00+00:00'); + $result = $span->update( + output: ['results' => 3], + endTime: '2025-06-01T12:00:00+00:00', + level: 'ERROR', + statusMessage: 'Failed', + version: '1.1.0', + ); // Assert - expect($result)->toBe($span) - ->and($history)->toHaveCount(2); - - expect(getEventType($history, index: 1))->toBe('span-update'); - $body = getEventBody($history, index: 1); - expect($body['id'])->toBe($span->id) + expect($result)->toBe($span) + ->and($history)->toHaveCount(2) + ->and(getEventType($history, index: 1))->toBe('span-update') + ->and($body['id'])->toBe($span->id) ->and($body['output'])->toBe(['results' => 3]) - ->and($body['endTime'])->toBe('2025-06-01T12:00:00+00:00'); + ->and($body['endTime'])->toBe('2025-06-01T12:00:00+00:00') + ->and($body['level'])->toBe('ERROR') + ->and($body['statusMessage'])->toBe('Failed') + ->and($body['version'])->toBe('1.1.0'); }); // ─── Generation ───────────────────────────────────────────────────────────── -it('creates a generation with full payload', function (): void { +it('creates a generation with all parameters', function (): void { // Arrange $history = []; $ingestion = makeIngestion($history); @@ -259,50 +255,51 @@ function getEventType(array $history, int $index = 0): string name: 'test-generation', input: ['messages' => [['role' => 'user', 'content' => 'Hi']]], output: 'Hello', - promptName: 'prompt-x', - promptVersion: 3, + generationId: 'custom-gen-id', + parentObservationId: 'span-abc', model: 'gpt-4o', modelParameters: ['temperature' => 0.2], + promptName: 'prompt-x', + promptVersion: 3, metadata: ['source' => 'test'], + startTime: '2025-01-01T00:00:00Z', + endTime: '2025-01-01T00:00:05Z', + completionStartTime: '2025-01-01T00:00:03Z', + usageDetails: ['input' => 10, 'output' => 20, 'total' => 30], + costDetails: ['input' => 0.001, 'output' => 0.002, 'total' => 0.003], + level: 'DEBUG', + statusMessage: 'All good', + version: '2.0.0', ); // Assert - expect($gen)->toBeInstanceOf(Generation::class) - ->and($history)->toHaveCount(1); - $body = getEventBody($history); - expect(getEventType($history))->toBe('generation-create') + expect($gen)->toBeInstanceOf(Generation::class) + ->and($gen->id)->toBe('custom-gen-id') + ->and($history)->toHaveCount(1) + ->and(getEventType($history))->toBe('generation-create') + ->and($body['id'])->toBe('custom-gen-id') ->and($body['traceId'])->toBe('my-trace-id') + ->and($body['parentObservationId'])->toBe('span-abc') ->and($body['name'])->toBe('test-generation') ->and($body['input'])->toBe(['messages' => [['role' => 'user', 'content' => 'Hi']]]) ->and($body['output'])->toBe('Hello') ->and($body['model'])->toBe('gpt-4o') + ->and($body['modelParameters'])->toBe(['temperature' => 0.2]) ->and($body['promptName'])->toBe('prompt-x') ->and($body['promptVersion'])->toBe(3) - ->and($body['modelParameters'])->toBe(['temperature' => 0.2]) - ->and($body['metadata'])->toBe(['source' => 'test']); -}); - -it('creates a generation with parentObservationId', function (): void { - // Arrange - $history = []; - $ingestion = makeIngestion($history); - - // Act - $ingestion->generation( - traceId: 'my-trace-id', - name: 'gen', - input: 'prompt', - output: 'response', - parentObservationId: 'span-abc', - ); - - // Assert - $body = getEventBody($history); - expect($body['parentObservationId'])->toBe('span-abc'); + ->and($body['metadata'])->toBe(['source' => 'test']) + ->and($body['startTime'])->toBe('2025-01-01T00:00:00Z') + ->and($body['endTime'])->toBe('2025-01-01T00:00:05Z') + ->and($body['completionStartTime'])->toBe('2025-01-01T00:00:03Z') + ->and($body['usageDetails'])->toBe(['input' => 10, 'output' => 20, 'total' => 30]) + ->and($body['costDetails'])->toBe(['input' => 0.001, 'output' => 0.002, 'total' => 0.003]) + ->and($body['level'])->toBe('DEBUG') + ->and($body['statusMessage'])->toBe('All good') + ->and($body['version'])->toBe('2.0.0'); }); -it('updates a generation with generation-update type', function (): void { +it('updates a generation with all parameters', function (): void { // Arrange $history = []; $ingestion = makeIngestion($history); @@ -314,17 +311,33 @@ function getEventType(array $history, int $index = 0): string input: 'prompt', output: 'initial', ); - $gen->update(output: 'updated response', model: 'gpt-4o'); + $result = $gen->update( + output: 'final', + model: 'gpt-4o', + endTime: '2025-01-01T00:00:05Z', + completionStartTime: '2025-01-01T00:00:03Z', + usageDetails: ['input' => 15, 'output' => 25, 'total' => 40], + costDetails: ['total' => 0.004], + level: 'WARNING', + statusMessage: 'Slow response', + version: '1.1.0', + ); // Assert - expect($history)->toHaveCount(2); - - expect(getEventType($history, index: 1))->toBe('generation-update'); - $body = getEventBody($history, index: 1); - expect($body['id'])->toBe($gen->id) - ->and($body['output'])->toBe('updated response') - ->and($body['model'])->toBe('gpt-4o'); + expect($result)->toBe($gen) + ->and($history)->toHaveCount(2) + ->and(getEventType($history, index: 1))->toBe('generation-update') + ->and($body['id'])->toBe($gen->id) + ->and($body['output'])->toBe('final') + ->and($body['model'])->toBe('gpt-4o') + ->and($body['endTime'])->toBe('2025-01-01T00:00:05Z') + ->and($body['completionStartTime'])->toBe('2025-01-01T00:00:03Z') + ->and($body['usageDetails'])->toBe(['input' => 15, 'output' => 25, 'total' => 40]) + ->and($body['costDetails'])->toBe(['total' => 0.004]) + ->and($body['level'])->toBe('WARNING') + ->and($body['statusMessage'])->toBe('Slow response') + ->and($body['version'])->toBe('1.1.0'); }); // ─── Trace child spawning ─────────────────────────────────────────────────── @@ -339,31 +352,38 @@ function getEventType(array $history, int $index = 0): string $span = $trace->span(name: 'child-span'); // Assert - expect($span)->toBeInstanceOf(Span::class) - ->and($history)->toHaveCount(2); - $body = getEventBody($history, index: 1); - expect($body['traceId'])->toBe($trace->id) + expect($span)->toBeInstanceOf(Span::class) + ->and($history)->toHaveCount(2) + ->and($body['traceId'])->toBe($trace->id) ->and($body['name'])->toBe('child-span'); }); -it('creates a generation from a trace', function (): void { +it('creates a generation from a trace with usageDetails and costDetails', function (): void { // Arrange $history = []; $ingestion = makeIngestion($history); // Act $trace = $ingestion->trace(name: 'my-trace'); - $gen = $trace->generation(name: 'llm-call', input: 'prompt', output: 'response'); + $gen = $trace->generation( + name: 'llm-call', + input: 'prompt', + output: 'response', + model: 'gpt-4o', + usageDetails: ['input' => 10, 'output' => 20], + costDetails: ['total' => 0.002], + ); // Assert - expect($gen)->toBeInstanceOf(Generation::class) - ->and($history)->toHaveCount(2); - $body = getEventBody($history, index: 1); - expect(getEventType($history, index: 1))->toBe('generation-create') + expect($gen)->toBeInstanceOf(Generation::class) + ->and($history)->toHaveCount(2) + ->and(getEventType($history, index: 1))->toBe('generation-create') ->and($body['traceId'])->toBe($trace->id) - ->and($body['name'])->toBe('llm-call'); + ->and($body['name'])->toBe('llm-call') + ->and($body['usageDetails'])->toBe(['input' => 10, 'output' => 20]) + ->and($body['costDetails'])->toBe(['total' => 0.002]); }); // ─── Span child spawning ─────────────────────────────────────────────────── @@ -379,15 +399,14 @@ function getEventType(array $history, int $index = 0): string $child = $parent->span(name: 'child-span'); // Assert - expect($history)->toHaveCount(3); - $body = getEventBody($history, index: 2); - expect($body['traceId'])->toBe($trace->id) + expect($history)->toHaveCount(3) + ->and($body['traceId'])->toBe($trace->id) ->and($body['parentObservationId'])->toBe($parent->id) ->and($body['name'])->toBe('child-span'); }); -it('creates a generation from a span with parentObservationId', function (): void { +it('creates a generation from a span with usageDetails and costDetails', function (): void { // Arrange $history = []; $ingestion = makeIngestion($history); @@ -395,15 +414,22 @@ function getEventType(array $history, int $index = 0): string // Act $trace = $ingestion->trace(name: 'my-trace'); $span = $trace->span(name: 'my-span'); - $gen = $span->generation(name: 'llm-call', input: 'prompt', output: 'response'); + $gen = $span->generation( + name: 'llm-call', + input: 'prompt', + output: 'response', + usageDetails: ['input' => 5, 'output' => 10], + costDetails: ['total' => 0.001], + ); // Assert - expect($history)->toHaveCount(3); - $body = getEventBody($history, index: 2); - expect(getEventType($history, index: 2))->toBe('generation-create') + expect($history)->toHaveCount(3) + ->and(getEventType($history, index: 2))->toBe('generation-create') ->and($body['traceId'])->toBe($trace->id) - ->and($body['parentObservationId'])->toBe($span->id); + ->and($body['parentObservationId'])->toBe($span->id) + ->and($body['usageDetails'])->toBe(['input' => 5, 'output' => 10]) + ->and($body['costDetails'])->toBe(['total' => 0.001]); }); // ─── Payload structure ───────────────────────────────────────────────────── @@ -419,12 +445,11 @@ function getEventType(array $history, int $index = 0): string // Assert $payload = getPayload($history); - expect($payload)->toHaveKey('batch') - ->and($payload['batch'])->toHaveCount(1); - /** @var array{batch: list}>} $payload */ $event = $payload['batch'][0]; - expect($event)->toHaveKey('id') + expect($payload)->toHaveKey('batch') + ->and($payload['batch'])->toHaveCount(1) + ->and($event)->toHaveKey('id') ->and($event)->toHaveKey('timestamp') ->and($event)->toHaveKey('type') ->and($event)->toHaveKey('body') @@ -432,11 +457,8 @@ function getEventType(array $history, int $index = 0): string }); it('generates valid uuid v4 format', function (): void { - // Act - $uuid = Ingestion::uuid(); - - // Assert - expect($uuid)->toMatch('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/'); + expect(Ingestion::uuid()) + ->toMatch('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/'); }); // ─── Error handling ───────────────────────────────────────────────────────── @@ -473,9 +495,8 @@ function getEventType(array $history, int $index = 0): string $trace->update(output: 'It is 22 degrees and sunny.'); // Assert: 7 HTTP calls (trace, span, child-span, child-update, generation, span-update, trace-update) - expect($history)->toHaveCount(7); - - expect(getEventType($history, index: 0))->toBe('trace-create') + expect($history)->toHaveCount(7) + ->and(getEventType($history, index: 0))->toBe('trace-create') ->and(getEventType($history, index: 1))->toBe('span-create') ->and(getEventType($history, index: 2))->toBe('span-create') ->and(getEventType($history, index: 3))->toBe('span-update') From 55f9d36aeaa21ab98cda76a1be4b8254612caaa4 Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Fri, 20 Feb 2026 13:37:20 +0100 Subject: [PATCH 2/2] chore: remove test:refactor dry-run --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9651b02..98892f9 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "test:lint": "@php vendor/bin/pint --config https://raw.githubusercontent.com/DIJ-digital/pint-config/main/pint.json", "test:unit": "pest", "test:types": "phpstan", - "test:refactor": "rector --dry-run", + "test:refactor": "rector", "test": [ "@test:lint", "@test:type-coverage",