From d60e53c58212c6f6474efabe29786c2a508eb223 Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Thu, 22 Jan 2026 13:24:37 +0100 Subject: [PATCH 1/6] feat: add Observation resource with get and list methods Implements GET /api/public/observations and GET /api/public/observations/{id} - ObservationResponse with full observation data including usage, costs, model info - ObservationListResponse with pagination metadata - Support for filtering by traceId, type, name, userId, timestamp, etc. - 4 tests covering get, list, filters, and usage data --- src/Langfuse.php | 10 +- src/Observation.php | 134 ++++++++++++++++++ src/Responses/ObservationListResponse.php | 62 ++++++++ src/Responses/ObservationResponse.php | 112 +++++++++++++++ .../Responses/GetObservationListResponse.php | 105 ++++++++++++++ .../Responses/GetObservationResponse.php | 66 +++++++++ tests/Feature/ObservationTest.php | 107 ++++++++++++++ 7 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 src/Observation.php create mode 100644 src/Responses/ObservationListResponse.php create mode 100644 src/Responses/ObservationResponse.php create mode 100644 src/Testing/Responses/GetObservationListResponse.php create mode 100644 src/Testing/Responses/GetObservationResponse.php create mode 100644 tests/Feature/ObservationTest.php diff --git a/src/Langfuse.php b/src/Langfuse.php index ebe96f9..462498f 100644 --- a/src/Langfuse.php +++ b/src/Langfuse.php @@ -12,8 +12,7 @@ public function __construct( private readonly TransporterInterface $transporter, private readonly string $environment = 'default', private readonly string $label = 'latest', - ) { - } + ) {} public function prompt(): Prompt { @@ -37,4 +36,11 @@ public function score(): Score transporter: $this->transporter, ); } + + public function observation(): Observation + { + return new Observation( + transporter: $this->transporter, + ); + } } diff --git a/src/Observation.php b/src/Observation.php new file mode 100644 index 0000000..feb8a44 --- /dev/null +++ b/src/Observation.php @@ -0,0 +1,134 @@ +transporter->get( + uri: sprintf('/api/public/observations/%s', urlencode($observationId)), + ); + + /** @var array{ + * id: string, + * traceId: string|null, + * type: string, + * name: string|null, + * startTime: string, + * endTime: string|null, + * completionStartTime: string|null, + * model: string|null, + * modelParameters: array|null, + * input: mixed, + * output: mixed, + * metadata: mixed, + * version: string|null, + * parentObservationId: string|null, + * level: string|null, + * statusMessage: string|null, + * promptId: string|null, + * promptName: string|null, + * promptVersion: int|null, + * usage: array{input: int|null, output: int|null, total: int|null, unit: string|null, inputCost: float|null, outputCost: float|null, totalCost: float|null}|null, + * calculatedInputCost: float|null, + * calculatedOutputCost: float|null, + * calculatedTotalCost: float|null, + * latency: float|null, + * timeToFirstToken: float|null, + * projectId: string, + * createdAt: string, + * updatedAt: string, + * environment: string|null + * } $data + */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return ObservationResponse::fromArray($data); + } + + /** + * @throws JsonException + */ + public function list( + ?int $page = null, + ?int $limit = null, + ?string $name = null, + ?string $userId = null, + ?string $traceId = null, + ?string $parentObservationId = null, + ?string $type = null, + ?string $fromStartTime = null, + ?string $toStartTime = null, + ?string $version = null, + ?string $environment = null, + ): ObservationListResponse { + $response = $this->transporter->get( + uri: '/api/public/observations', + options: ['query' => array_filter([ + 'page' => $page, + 'limit' => $limit, + 'name' => $name, + 'userId' => $userId, + 'traceId' => $traceId, + 'parentObservationId' => $parentObservationId, + 'type' => $type, + 'fromStartTime' => $fromStartTime, + 'toStartTime' => $toStartTime, + 'version' => $version, + 'environment' => $environment, + ])] + ); + + /** @var array{ + * data: array|null, + * input: mixed, + * output: mixed, + * metadata: mixed, + * version: string|null, + * parentObservationId: string|null, + * level: string|null, + * statusMessage: string|null, + * promptId: string|null, + * promptName: string|null, + * promptVersion: int|null, + * usage: array{input: int|null, output: int|null, total: int|null, unit: string|null, inputCost: float|null, outputCost: float|null, totalCost: float|null}|null, + * calculatedInputCost: float|null, + * calculatedOutputCost: float|null, + * calculatedTotalCost: float|null, + * latency: float|null, + * timeToFirstToken: float|null, + * projectId: string, + * createdAt: string, + * updatedAt: string, + * environment: string|null + * }>, + * meta: array{page: int, limit: int, totalPages: int, totalItems: int} + * } $data + */ + $data = json_decode($response->getBody()->getContents(), true, flags: JSON_THROW_ON_ERROR); + + return ObservationListResponse::fromArray($data); + } +} diff --git a/src/Responses/ObservationListResponse.php b/src/Responses/ObservationListResponse.php new file mode 100644 index 0000000..1502af2 --- /dev/null +++ b/src/Responses/ObservationListResponse.php @@ -0,0 +1,62 @@ + $data + */ + public function __construct( + public array $data, + public MetaData $meta, + ) {} + + /** + * @param array{ + * data: array|null, + * input: mixed, + * output: mixed, + * metadata: mixed, + * version: string|null, + * parentObservationId: string|null, + * level: string|null, + * statusMessage: string|null, + * promptId: string|null, + * promptName: string|null, + * promptVersion: int|null, + * usage?: array{input: int|null, output: int|null, total: int|null, unit: string|null, inputCost: float|null, outputCost: float|null, totalCost: float|null}|null, + * calculatedInputCost?: float|null, + * calculatedOutputCost?: float|null, + * calculatedTotalCost?: float|null, + * latency?: float|null, + * timeToFirstToken?: float|null, + * projectId: string, + * createdAt: string, + * updatedAt: string, + * environment?: string|null + * }>, + * meta: array{page: int, limit: int, totalPages: int, totalItems: int} + * } $data + */ + public static function fromArray(array $data): self + { + return new self( + data: array_map(fn (array $item): ObservationResponse => ObservationResponse::fromArray($item), $data['data']), + meta: MetaData::fromArray($data['meta']), + ); + } +} diff --git a/src/Responses/ObservationResponse.php b/src/Responses/ObservationResponse.php new file mode 100644 index 0000000..9345caf --- /dev/null +++ b/src/Responses/ObservationResponse.php @@ -0,0 +1,112 @@ +|null $modelParameters + * @param array{input: int|null, output: int|null, total: int|null, unit: string|null, inputCost: float|null, outputCost: float|null, totalCost: float|null}|null $usage + */ + public function __construct( + public string $id, + public ?string $traceId, + public string $type, + public ?string $name, + public string $startTime, + public ?string $endTime, + public ?string $completionStartTime, + public ?string $model, + public ?array $modelParameters, + public mixed $input, + public mixed $output, + public mixed $metadata, + public ?string $version, + public ?string $parentObservationId, + public ?string $level, + public ?string $statusMessage, + public ?string $promptId, + public ?string $promptName, + public ?int $promptVersion, + public ?array $usage, + public ?float $calculatedInputCost, + public ?float $calculatedOutputCost, + public ?float $calculatedTotalCost, + public ?float $latency, + public ?float $timeToFirstToken, + public string $projectId, + public string $createdAt, + public string $updatedAt, + public ?string $environment = null, + ) {} + + /** + * @param array{ + * id: string, + * traceId: string|null, + * type: string, + * name: string|null, + * startTime: string, + * endTime: string|null, + * completionStartTime: string|null, + * model: string|null, + * modelParameters: array|null, + * input: mixed, + * output: mixed, + * metadata: mixed, + * version: string|null, + * parentObservationId: string|null, + * level: string|null, + * statusMessage: string|null, + * promptId: string|null, + * promptName: string|null, + * promptVersion: int|null, + * usage?: array{input: int|null, output: int|null, total: int|null, unit: string|null, inputCost: float|null, outputCost: float|null, totalCost: float|null}|null, + * calculatedInputCost?: float|null, + * calculatedOutputCost?: float|null, + * calculatedTotalCost?: float|null, + * latency?: float|null, + * timeToFirstToken?: float|null, + * projectId: string, + * createdAt: string, + * updatedAt: string, + * environment?: string|null + * } $data + */ + public static function fromArray(array $data): self + { + return new self( + id: $data['id'], + traceId: $data['traceId'], + type: $data['type'], + name: $data['name'], + startTime: $data['startTime'], + endTime: $data['endTime'], + completionStartTime: $data['completionStartTime'], + model: $data['model'], + modelParameters: $data['modelParameters'], + input: $data['input'], + output: $data['output'], + metadata: $data['metadata'], + version: $data['version'], + parentObservationId: $data['parentObservationId'], + level: $data['level'], + statusMessage: $data['statusMessage'], + promptId: $data['promptId'], + promptName: $data['promptName'], + promptVersion: $data['promptVersion'], + usage: $data['usage'] ?? null, + calculatedInputCost: $data['calculatedInputCost'] ?? null, + calculatedOutputCost: $data['calculatedOutputCost'] ?? null, + calculatedTotalCost: $data['calculatedTotalCost'] ?? null, + latency: $data['latency'] ?? null, + timeToFirstToken: $data['timeToFirstToken'] ?? null, + projectId: $data['projectId'], + createdAt: $data['createdAt'], + updatedAt: $data['updatedAt'], + environment: $data['environment'] ?? null, + ); + } +} diff --git a/src/Testing/Responses/GetObservationListResponse.php b/src/Testing/Responses/GetObservationListResponse.php new file mode 100644 index 0000000..3fe9a1a --- /dev/null +++ b/src/Testing/Responses/GetObservationListResponse.php @@ -0,0 +1,105 @@ +|string> $headers + */ + public function __construct(int $status = 200, array $headers = [], string $version = '1.1', ?string $reason = null) + { + parent::__construct($status, $headers, (string) json_encode($this->payload()), $version, $reason); + } + + /** + * @return array + */ + public function payload(): array + { + return [ + 'data' => [ + [ + 'id' => 'obs-abc123', + 'traceId' => 'trace-abc123', + 'type' => 'GENERATION', + 'name' => 'llm-generation-1', + 'startTime' => '2025-01-22T10:30:00.000Z', + 'endTime' => '2025-01-22T10:30:02.500Z', + 'completionStartTime' => '2025-01-22T10:30:00.500Z', + 'model' => 'gpt-4o', + 'modelParameters' => ['temperature' => 0.7], + 'input' => ['prompt' => 'Hello'], + 'output' => ['response' => 'Hi there!'], + 'metadata' => null, + 'version' => 'v1', + 'parentObservationId' => null, + 'level' => 'DEFAULT', + 'statusMessage' => null, + 'promptId' => null, + 'promptName' => null, + 'promptVersion' => null, + 'usage' => [ + 'input' => 10, + 'output' => 25, + 'total' => 35, + 'unit' => 'TOKENS', + 'inputCost' => 0.0001, + 'outputCost' => 0.0005, + 'totalCost' => 0.0006, + ], + 'calculatedInputCost' => 0.0001, + 'calculatedOutputCost' => 0.0005, + 'calculatedTotalCost' => 0.0006, + 'latency' => 2.5, + 'timeToFirstToken' => 0.5, + 'projectId' => 'proj-abc123', + 'createdAt' => '2025-01-22T10:30:00.000Z', + 'updatedAt' => '2025-01-22T10:30:02.500Z', + 'environment' => 'production', + ], + [ + 'id' => 'obs-def456', + 'traceId' => 'trace-abc123', + 'type' => 'SPAN', + 'name' => 'retrieval-span', + 'startTime' => '2025-01-22T10:29:58.000Z', + 'endTime' => '2025-01-22T10:29:59.500Z', + 'completionStartTime' => null, + 'model' => null, + 'modelParameters' => null, + 'input' => ['query' => 'search term'], + 'output' => ['results' => [['id' => 1], ['id' => 2]]], + 'metadata' => ['source' => 'vector-db'], + 'version' => 'v1', + 'parentObservationId' => null, + 'level' => 'DEFAULT', + 'statusMessage' => null, + 'promptId' => null, + 'promptName' => null, + 'promptVersion' => null, + 'usage' => null, + 'calculatedInputCost' => null, + 'calculatedOutputCost' => null, + 'calculatedTotalCost' => null, + 'latency' => 1.5, + 'timeToFirstToken' => null, + 'projectId' => 'proj-abc123', + 'createdAt' => '2025-01-22T10:29:58.000Z', + 'updatedAt' => '2025-01-22T10:29:59.500Z', + 'environment' => 'production', + ], + ], + 'meta' => [ + 'page' => 1, + 'limit' => 10, + 'totalPages' => 1, + 'totalItems' => 2, + ], + ]; + } +} diff --git a/src/Testing/Responses/GetObservationResponse.php b/src/Testing/Responses/GetObservationResponse.php new file mode 100644 index 0000000..6935bb5 --- /dev/null +++ b/src/Testing/Responses/GetObservationResponse.php @@ -0,0 +1,66 @@ +|string> $headers + * @param array $data + */ + public function __construct(int $status = 200, array $headers = [], string $version = '1.1', ?string $reason = null, array $data = []) + { + parent::__construct($status, $headers, (string) json_encode($this->payload($data)), $version, $reason); + } + + /** + * @param array $data + * @return array + */ + public function payload(array $data = []): array + { + return array_merge([ + 'id' => 'obs-abc123', + 'traceId' => 'trace-abc123', + 'type' => 'GENERATION', + 'name' => 'llm-generation', + 'startTime' => '2025-01-22T10:30:00.000Z', + 'endTime' => '2025-01-22T10:30:02.500Z', + 'completionStartTime' => '2025-01-22T10:30:00.500Z', + 'model' => 'gpt-4o', + 'modelParameters' => ['temperature' => 0.7, 'max_tokens' => 1000], + 'input' => ['messages' => [['role' => 'user', 'content' => 'Hello']]], + 'output' => ['content' => 'Hi there! How can I help you?'], + 'metadata' => ['key' => 'value'], + 'version' => 'v1', + 'parentObservationId' => null, + 'level' => 'DEFAULT', + 'statusMessage' => null, + 'promptId' => 'prompt-123', + 'promptName' => 'greeting-prompt', + 'promptVersion' => 1, + 'usage' => [ + 'input' => 10, + 'output' => 25, + 'total' => 35, + 'unit' => 'TOKENS', + 'inputCost' => 0.0001, + 'outputCost' => 0.0005, + 'totalCost' => 0.0006, + ], + 'calculatedInputCost' => 0.0001, + 'calculatedOutputCost' => 0.0005, + 'calculatedTotalCost' => 0.0006, + 'latency' => 2.5, + 'timeToFirstToken' => 0.5, + 'projectId' => 'proj-abc123', + 'createdAt' => '2025-01-22T10:30:00.000Z', + 'updatedAt' => '2025-01-22T10:30:02.500Z', + 'environment' => 'production', + ], $data); + } +} diff --git a/tests/Feature/ObservationTest.php b/tests/Feature/ObservationTest.php new file mode 100644 index 0000000..22e97b8 --- /dev/null +++ b/tests/Feature/ObservationTest.php @@ -0,0 +1,107 @@ + $handlerStack]); + + $observation = (new Langfuse(new HttpTransporter($client))) + ->observation() + ->get('obs-abc123'); + + expect($observation)->toBeInstanceOf(ObservationResponse::class) + ->and($observation->id)->toBe('obs-abc123') + ->and($observation->traceId)->toBe('trace-abc123') + ->and($observation->type)->toBe('GENERATION') + ->and($observation->name)->toBe('llm-generation') + ->and($observation->model)->toBe('gpt-4o') + ->and($observation->latency)->toBe(2.5) + ->and($observation->environment)->toBe('production'); +}); + +it('can list observations', function (): void { + $mock = new MockHandler([ + new GetObservationListResponse, + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $observations = (new Langfuse(new HttpTransporter($client))) + ->observation() + ->list(); + + expect($observations)->toBeInstanceOf(ObservationListResponse::class) + ->and($observations->data)->toBeArray() + ->and($observations->data)->toHaveCount(2) + ->and($observations->data[0])->toBeInstanceOf(ObservationResponse::class) + ->and($observations->data[0]->id)->toBe('obs-abc123') + ->and($observations->data[0]->type)->toBe('GENERATION') + ->and($observations->data[1]->id)->toBe('obs-def456') + ->and($observations->data[1]->type)->toBe('SPAN') + ->and($observations->meta)->toBeInstanceOf(MetaData::class) + ->and($observations->meta->totalItems)->toBe(2); +}); + +it('can list observations with filters', function (): void { + $mock = new MockHandler([ + new GetObservationListResponse, + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $observations = (new Langfuse(new HttpTransporter($client))) + ->observation() + ->list( + page: 1, + limit: 10, + traceId: 'trace-abc123', + type: 'GENERATION', + environment: 'production', + ); + + expect($observations)->toBeInstanceOf(ObservationListResponse::class) + ->and($observations->data)->toBeArray(); +}); + +it('can get observation with usage data', function (): void { + $mock = new MockHandler([ + new GetObservationResponse, + ]); + + $handlerStack = HandlerStack::create($mock); + $client = new Client(['handler' => $handlerStack]); + + $observation = (new Langfuse(new HttpTransporter($client))) + ->observation() + ->get('obs-abc123'); + + expect($observation->usage)->toBeArray() + ->and($observation->usage)->not->toBeEmpty() + ->and($observation->calculatedTotalCost)->toBe(0.0006) + ->and($observation->promptName)->toBe('greeting-prompt') + ->and($observation->promptVersion)->toBe(1); + + /** @var array{input: int, output: int, total: int} $usage */ + $usage = $observation->usage; + expect($usage['input'])->toBe(10) + ->and($usage['output'])->toBe(25) + ->and($usage['total'])->toBe(35); +}); From 8731958b08864ee034ff9be655cc527091b60532 Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Fri, 23 Jan 2026 16:24:31 +0100 Subject: [PATCH 2/6] fix: align prompts and observation filters --- src/Contracts/TransporterInterface.php | 16 ++++++++-------- src/Observation.php | 4 ++++ src/Transporters/HttpTransporter.php | 13 ++++++------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Contracts/TransporterInterface.php b/src/Contracts/TransporterInterface.php index 8234ade..558497a 100644 --- a/src/Contracts/TransporterInterface.php +++ b/src/Contracts/TransporterInterface.php @@ -9,34 +9,34 @@ interface TransporterInterface { /** - * @param array $options + * @param array $options */ public function request(string $method, string $uri, array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function get(string $uri, array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function post(string $uri, array $options = []): ResponseInterface; /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options */ public function postJson(string $uri, array $data = [], array $options = []): ResponseInterface; /** - * @param array $options + * @param array $options */ public function delete(string $uri, array $options = []): ResponseInterface; /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options */ public function patchJson(string $uri, array $data = [], array $options = []): ResponseInterface; } diff --git a/src/Observation.php b/src/Observation.php index feb8a44..391ac52 100644 --- a/src/Observation.php +++ b/src/Observation.php @@ -74,6 +74,8 @@ public function list( ?string $toStartTime = null, ?string $version = null, ?string $environment = null, + ?string $level = null, + ?string $filter = null, ): ObservationListResponse { $response = $this->transporter->get( uri: '/api/public/observations', @@ -89,6 +91,8 @@ public function list( 'toStartTime' => $toStartTime, 'version' => $version, 'environment' => $environment, + 'level' => $level, + 'filter' => $filter, ])] ); diff --git a/src/Transporters/HttpTransporter.php b/src/Transporters/HttpTransporter.php index 319cf49..a2b734b 100644 --- a/src/Transporters/HttpTransporter.php +++ b/src/Transporters/HttpTransporter.php @@ -21,8 +21,7 @@ class HttpTransporter implements TransporterInterface { public function __construct( public readonly ClientInterface $client - ) { - } + ) {} /** * @throws BadRequestException @@ -45,7 +44,7 @@ public function request(string $method, string $uri, array $options = []): Respo } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -61,7 +60,7 @@ public function get(string $uri, array $options = []): ResponseInterface } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -82,7 +81,7 @@ public function postJson(string $uri, array $data = [], array $options = []): Re } /** - * @param array $options + * @param array $options * * @throws BadRequestException * @throws ForbiddenException @@ -98,8 +97,8 @@ public function delete(string $uri, array $options = []): ResponseInterface } /** - * @param array $data - * @param array $options + * @param array $data + * @param array $options * * @throws BadRequestException * @throws ForbiddenException From d14a8b9311b190cb21b0cd940231a0eba561e1d6 Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Fri, 23 Jan 2026 18:05:01 +0100 Subject: [PATCH 3/6] refactor: update observations to v2 API with cursor pagination and fields --- src/Observation.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Observation.php b/src/Observation.php index 391ac52..e29f5b9 100644 --- a/src/Observation.php +++ b/src/Observation.php @@ -19,7 +19,7 @@ public function __construct(private readonly TransporterInterface $transporter) public function get(string $observationId): ObservationResponse { $response = $this->transporter->get( - uri: sprintf('/api/public/observations/%s', urlencode($observationId)), + uri: sprintf('/api/public/v2/observations/%s', urlencode($observationId)), ); /** @var array{ @@ -78,7 +78,7 @@ public function list( ?string $filter = null, ): ObservationListResponse { $response = $this->transporter->get( - uri: '/api/public/observations', + uri: '/api/public/v2/observations', options: ['query' => array_filter([ 'page' => $page, 'limit' => $limit, From f36b12f50030b26bef2bdf85b6c5e0cdd776b3ed Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Fri, 23 Jan 2026 18:05:23 +0100 Subject: [PATCH 4/6] refactor: update observations to v2 API with cursor pagination and fields --- src/Observation.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Observation.php b/src/Observation.php index e29f5b9..150bf7e 100644 --- a/src/Observation.php +++ b/src/Observation.php @@ -63,7 +63,7 @@ public function get(string $observationId): ObservationResponse * @throws JsonException */ public function list( - ?int $page = null, + ?string $cursor = null, ?int $limit = null, ?string $name = null, ?string $userId = null, @@ -76,11 +76,12 @@ public function list( ?string $environment = null, ?string $level = null, ?string $filter = null, + ?string $fields = null, ): ObservationListResponse { $response = $this->transporter->get( uri: '/api/public/v2/observations', options: ['query' => array_filter([ - 'page' => $page, + 'cursor' => $cursor, 'limit' => $limit, 'name' => $name, 'userId' => $userId, @@ -93,6 +94,7 @@ public function list( 'environment' => $environment, 'level' => $level, 'filter' => $filter, + 'fields' => $fields, ])] ); From cd49c68f36e74b69c14848d6b2ff28f9382db29c Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Fri, 23 Jan 2026 18:42:06 +0100 Subject: [PATCH 5/6] align observations with openapi endpoints --- src/Observation.php | 22 ++++++++-------- .../Responses/GetObservationListResponse.php | 1 + tests/Feature/ObservationTest.php | 25 ++++++++++++++++--- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/Observation.php b/src/Observation.php index 150bf7e..27f6955 100644 --- a/src/Observation.php +++ b/src/Observation.php @@ -19,7 +19,7 @@ public function __construct(private readonly TransporterInterface $transporter) public function get(string $observationId): ObservationResponse { $response = $this->transporter->get( - uri: sprintf('/api/public/v2/observations/%s', urlencode($observationId)), + uri: sprintf('/api/public/observations/%s', urlencode($observationId)), ); /** @var array{ @@ -63,38 +63,36 @@ public function get(string $observationId): ObservationResponse * @throws JsonException */ public function list( - ?string $cursor = null, + ?int $page = null, ?int $limit = null, ?string $name = null, ?string $userId = null, + ?string $type = null, ?string $traceId = null, + ?string $level = null, ?string $parentObservationId = null, - ?string $type = null, + ?array $environment = null, ?string $fromStartTime = null, ?string $toStartTime = null, ?string $version = null, - ?string $environment = null, - ?string $level = null, ?string $filter = null, - ?string $fields = null, ): ObservationListResponse { $response = $this->transporter->get( - uri: '/api/public/v2/observations', + uri: '/api/public/observations', options: ['query' => array_filter([ - 'cursor' => $cursor, + 'page' => $page, 'limit' => $limit, 'name' => $name, 'userId' => $userId, + 'type' => $type, 'traceId' => $traceId, + 'level' => $level, 'parentObservationId' => $parentObservationId, - 'type' => $type, + 'environment' => $environment, 'fromStartTime' => $fromStartTime, 'toStartTime' => $toStartTime, 'version' => $version, - 'environment' => $environment, - 'level' => $level, 'filter' => $filter, - 'fields' => $fields, ])] ); diff --git a/src/Testing/Responses/GetObservationListResponse.php b/src/Testing/Responses/GetObservationListResponse.php index 3fe9a1a..7bba8cf 100644 --- a/src/Testing/Responses/GetObservationListResponse.php +++ b/src/Testing/Responses/GetObservationListResponse.php @@ -101,5 +101,6 @@ public function payload(): array 'totalItems' => 2, ], ]; + } } diff --git a/tests/Feature/ObservationTest.php b/tests/Feature/ObservationTest.php index 22e97b8..4c115a1 100644 --- a/tests/Feature/ObservationTest.php +++ b/tests/Feature/ObservationTest.php @@ -12,10 +12,12 @@ use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use Psr\Http\Message\RequestInterface; it('can get an observation by id', function (): void { $mock = new MockHandler([ - new GetObservationResponse, + new GetObservationResponse(), ]); $handlerStack = HandlerStack::create($mock); @@ -60,11 +62,15 @@ }); it('can list observations with filters', function (): void { + /** @var array $history */ + $history = []; + $mock = new MockHandler([ new GetObservationListResponse, ]); $handlerStack = HandlerStack::create($mock); + $handlerStack->push(Middleware::history($history)); $client = new Client(['handler' => $handlerStack]); $observations = (new Langfuse(new HttpTransporter($client))) @@ -74,16 +80,29 @@ limit: 10, traceId: 'trace-abc123', type: 'GENERATION', - environment: 'production', + environment: ['production'], ); expect($observations)->toBeInstanceOf(ObservationListResponse::class) ->and($observations->data)->toBeArray(); + + expect($history)->toHaveCount(1); + + /** @var array{request: RequestInterface} $first */ + $first = $history[0]; + /** @var RequestInterface $request */ + $request = $first['request']; + $query = $request->getUri()->getQuery(); + + expect($query)->toContain('page=1') + ->and($query)->toContain('limit=10') + ->and($query)->toContain('traceId=trace-abc123') + ->and($query)->toContain('type=GENERATION'); }); it('can get observation with usage data', function (): void { $mock = new MockHandler([ - new GetObservationResponse, + new GetObservationResponse(), ]); $handlerStack = HandlerStack::create($mock); From f29a8b58ce1307f0b34b14b89780ca56f3cd68ba Mon Sep 17 00:00:00 2001 From: Tycho Engberink Date: Fri, 20 Feb 2026 13:37:19 +0100 Subject: [PATCH 6/6] 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",