diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f391d41..27d2fbb 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.6.0" + ".": "3.7.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 1e55acc..094239e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-1c405024b4a17886e921871250a445362463b7ae6070cacc3d1927d59036730c.yml -openapi_spec_hash: c4ea3735257a48ed105002eb7aaa095a -config_hash: 85d56c7c196269badbd0b4a9dfb28d45 +configured_endpoints: 8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-089c8670f1d7c2e9fa8e5c97010db7c24b8f162eb7cfe76ffa41d70fa46efe2f.yml +openapi_spec_hash: 7a226aee8f3f2ab16febbe6bb35e1657 +config_hash: 8e4ed6629c178aa0c8aaf575cb07c544 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2029652..b71f457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 3.7.0 (2026-01-22) + +Full Changelog: [v3.6.0...v3.7.0](https://github.com/browserbase/stagehand-php/compare/v3.6.0...v3.7.0) + +### Features + +* Include replay endpoint in stainless spec so SDK clients can get run metrics ([7aa307a](https://github.com/browserbase/stagehand-php/commit/7aa307a606dab73325780ea7652ed649d5d4ab30)) + + +### Chores + +* **internal:** update phpstan comments ([544e99a](https://github.com/browserbase/stagehand-php/commit/544e99a2230737264bbd381c6d4b62974e4df5ed)) + ## 3.6.0 (2026-01-21) Full Changelog: [v3.5.0...v3.6.0](https://github.com/browserbase/stagehand-php/compare/v3.5.0...v3.6.0) diff --git a/src/Core/BaseClient.php b/src/Core/BaseClient.php index eb7c6dd..6d944db 100644 --- a/src/Core/BaseClient.php +++ b/src/Core/BaseClient.php @@ -71,8 +71,17 @@ public function request( ?string $stream = null, RequestOptions|array|null $options = [], ): BaseResponse { - // @phpstan-ignore-next-line - [$req, $opts] = $this->buildRequest(method: $method, path: $path, query: $query, headers: $headers, body: $body, opts: $options); + [$req, $opts] = $this->buildRequest( + method: $method, + // @phpstan-ignore argument.type + path: $path, + query: $query, + // @phpstan-ignore argument.type + headers: $headers, + body: $body, + // @phpstan-ignore argument.type + opts: $options, + ); ['method' => $method, 'path' => $uri, 'headers' => $headers, 'body' => $data] = $req; assert(!is_null($opts->requestFactory)); diff --git a/src/ServiceContracts/SessionsContract.php b/src/ServiceContracts/SessionsContract.php index 0fb46ad..0cd0ad2 100644 --- a/src/ServiceContracts/SessionsContract.php +++ b/src/ServiceContracts/SessionsContract.php @@ -18,6 +18,7 @@ use Stagehand\Sessions\SessionExtractResponse; use Stagehand\Sessions\SessionNavigateResponse; use Stagehand\Sessions\SessionObserveResponse; +use Stagehand\Sessions\SessionReplayResponse; use Stagehand\Sessions\SessionStartParams\Browser; use Stagehand\Sessions\SessionStartParams\BrowserbaseSessionCreateParams; use Stagehand\Sessions\SessionStartResponse; @@ -259,6 +260,21 @@ public function observeStream( RequestOptions|array|null $requestOptions = null, ): BaseStream; + /** + * @api + * + * @param string $id Unique session identifier + * @param \Stagehand\Sessions\SessionReplayParams\XStreamResponse|value-of<\Stagehand\Sessions\SessionReplayParams\XStreamResponse> $xStreamResponse Whether to stream the response via SSE + * @param RequestOpts|null $requestOptions + * + * @throws APIException + */ + public function replay( + string $id, + \Stagehand\Sessions\SessionReplayParams\XStreamResponse|string|null $xStreamResponse = null, + RequestOptions|array|null $requestOptions = null, + ): SessionReplayResponse; + /** * @api * diff --git a/src/ServiceContracts/SessionsRawContract.php b/src/ServiceContracts/SessionsRawContract.php index af38c79..3129a6d 100644 --- a/src/ServiceContracts/SessionsRawContract.php +++ b/src/ServiceContracts/SessionsRawContract.php @@ -20,6 +20,8 @@ use Stagehand\Sessions\SessionNavigateResponse; use Stagehand\Sessions\SessionObserveParams; use Stagehand\Sessions\SessionObserveResponse; +use Stagehand\Sessions\SessionReplayParams; +use Stagehand\Sessions\SessionReplayResponse; use Stagehand\Sessions\SessionStartParams; use Stagehand\Sessions\SessionStartResponse; use Stagehand\Sessions\StreamEvent; @@ -199,6 +201,23 @@ public function observeStream( RequestOptions|array|null $requestOptions = null, ): BaseResponse; + /** + * @api + * + * @param string $id Unique session identifier + * @param array|SessionReplayParams $params + * @param RequestOpts|null $requestOptions + * + * @return BaseResponse + * + * @throws APIException + */ + public function replay( + string $id, + array|SessionReplayParams $params, + RequestOptions|array|null $requestOptions = null, + ): BaseResponse; + /** * @api * diff --git a/src/Services/SessionsRawService.php b/src/Services/SessionsRawService.php index 7319162..4482975 100644 --- a/src/Services/SessionsRawService.php +++ b/src/Services/SessionsRawService.php @@ -27,6 +27,8 @@ use Stagehand\Sessions\SessionNavigateResponse; use Stagehand\Sessions\SessionObserveParams; use Stagehand\Sessions\SessionObserveResponse; +use Stagehand\Sessions\SessionReplayParams; +use Stagehand\Sessions\SessionReplayResponse; use Stagehand\Sessions\SessionStartParams; use Stagehand\Sessions\SessionStartParams\Browser; use Stagehand\Sessions\SessionStartParams\BrowserbaseSessionCreateParams; @@ -535,6 +537,44 @@ public function observeStream( ); } + /** + * @api + * + * Retrieves replay metrics for a session. + * + * @param string $id Unique session identifier + * @param array{ + * xStreamResponse?: SessionReplayParams\XStreamResponse|value-of, + * }|SessionReplayParams $params + * @param RequestOpts|null $requestOptions + * + * @return BaseResponse + * + * @throws APIException + */ + public function replay( + string $id, + array|SessionReplayParams $params, + RequestOptions|array|null $requestOptions = null, + ): BaseResponse { + [$parsed, $options] = SessionReplayParams::parseRequest( + $params, + $requestOptions, + ); + + // @phpstan-ignore-next-line return.type + return $this->client->request( + method: 'get', + path: ['v1/sessions/%1$s/replay', $id], + headers: Util::array_transform_keys( + $parsed, + ['xStreamResponse' => 'x-stream-response'] + ), + options: $options, + convert: SessionReplayResponse::class, + ); + } + /** * @api * diff --git a/src/Services/SessionsService.php b/src/Services/SessionsService.php index 12ec98b..f993409 100644 --- a/src/Services/SessionsService.php +++ b/src/Services/SessionsService.php @@ -21,6 +21,7 @@ use Stagehand\Sessions\SessionExtractResponse; use Stagehand\Sessions\SessionNavigateResponse; use Stagehand\Sessions\SessionObserveResponse; +use Stagehand\Sessions\SessionReplayResponse; use Stagehand\Sessions\SessionStartParams\Browser; use Stagehand\Sessions\SessionStartParams\BrowserbaseSessionCreateParams; use Stagehand\Sessions\SessionStartResponse; @@ -425,6 +426,30 @@ public function observeStream( return $response->parse(); } + /** + * @api + * + * Retrieves replay metrics for a session. + * + * @param string $id Unique session identifier + * @param \Stagehand\Sessions\SessionReplayParams\XStreamResponse|value-of<\Stagehand\Sessions\SessionReplayParams\XStreamResponse> $xStreamResponse Whether to stream the response via SSE + * @param RequestOpts|null $requestOptions + * + * @throws APIException + */ + public function replay( + string $id, + \Stagehand\Sessions\SessionReplayParams\XStreamResponse|string|null $xStreamResponse = null, + RequestOptions|array|null $requestOptions = null, + ): SessionReplayResponse { + $params = Util::removeNulls(['xStreamResponse' => $xStreamResponse]); + + // @phpstan-ignore-next-line argument.type + $response = $this->raw->replay($id, params: $params, requestOptions: $requestOptions); + + return $response->parse(); + } + /** * @api * diff --git a/src/Sessions/SessionReplayParams.php b/src/Sessions/SessionReplayParams.php new file mode 100644 index 0000000..80c8e56 --- /dev/null +++ b/src/Sessions/SessionReplayParams.php @@ -0,0 +1,71 @@ + + * } + */ +final class SessionReplayParams implements BaseModel +{ + /** @use SdkModel */ + use SdkModel; + use SdkParams; + + /** + * Whether to stream the response via SSE. + * + * @var value-of|null $xStreamResponse + */ + #[Optional(enum: XStreamResponse::class)] + public ?string $xStreamResponse; + + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + * + * @param XStreamResponse|value-of|null $xStreamResponse + */ + public static function with( + XStreamResponse|string|null $xStreamResponse = null + ): self { + $self = new self; + + null !== $xStreamResponse && $self['xStreamResponse'] = $xStreamResponse; + + return $self; + } + + /** + * Whether to stream the response via SSE. + * + * @param XStreamResponse|value-of $xStreamResponse + */ + public function withXStreamResponse( + XStreamResponse|string $xStreamResponse + ): self { + $self = clone $this; + $self['xStreamResponse'] = $xStreamResponse; + + return $self; + } +} diff --git a/src/Sessions/SessionReplayParams/XStreamResponse.php b/src/Sessions/SessionReplayParams/XStreamResponse.php new file mode 100644 index 0000000..6814a5a --- /dev/null +++ b/src/Sessions/SessionReplayParams/XStreamResponse.php @@ -0,0 +1,15 @@ + */ + use SdkModel; + + #[Required] + public Data $data; + + /** + * Indicates whether the request was successful. + */ + #[Required] + public bool $success; + + /** + * `new SessionReplayResponse()` is missing required properties by the API. + * + * To enforce required parameters use + * ``` + * SessionReplayResponse::with(data: ..., success: ...) + * ``` + * + * Otherwise ensure the following setters are called + * + * ``` + * (new SessionReplayResponse)->withData(...)->withSuccess(...) + * ``` + */ + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + * + * @param Data|DataShape $data + */ + public static function with(Data|array $data, bool $success): self + { + $self = new self; + + $self['data'] = $data; + $self['success'] = $success; + + return $self; + } + + /** + * @param Data|DataShape $data + */ + public function withData(Data|array $data): self + { + $self = clone $this; + $self['data'] = $data; + + return $self; + } + + /** + * Indicates whether the request was successful. + */ + public function withSuccess(bool $success): self + { + $self = clone $this; + $self['success'] = $success; + + return $self; + } +} diff --git a/src/Sessions/SessionReplayResponse/Data.php b/src/Sessions/SessionReplayResponse/Data.php new file mode 100644 index 0000000..2a1a640 --- /dev/null +++ b/src/Sessions/SessionReplayResponse/Data.php @@ -0,0 +1,57 @@ +|null} + */ +final class Data implements BaseModel +{ + /** @use SdkModel */ + use SdkModel; + + /** @var list|null $pages */ + #[Optional(list: Page::class)] + public ?array $pages; + + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + * + * @param list|null $pages + */ + public static function with(?array $pages = null): self + { + $self = new self; + + null !== $pages && $self['pages'] = $pages; + + return $self; + } + + /** + * @param list $pages + */ + public function withPages(array $pages): self + { + $self = clone $this; + $self['pages'] = $pages; + + return $self; + } +} diff --git a/src/Sessions/SessionReplayResponse/Data/Page.php b/src/Sessions/SessionReplayResponse/Data/Page.php new file mode 100644 index 0000000..46a4c66 --- /dev/null +++ b/src/Sessions/SessionReplayResponse/Data/Page.php @@ -0,0 +1,57 @@ +|null} + */ +final class Page implements BaseModel +{ + /** @use SdkModel */ + use SdkModel; + + /** @var list|null $actions */ + #[Optional(list: Action::class)] + public ?array $actions; + + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + * + * @param list|null $actions + */ + public static function with(?array $actions = null): self + { + $self = new self; + + null !== $actions && $self['actions'] = $actions; + + return $self; + } + + /** + * @param list $actions + */ + public function withActions(array $actions): self + { + $self = clone $this; + $self['actions'] = $actions; + + return $self; + } +} diff --git a/src/Sessions/SessionReplayResponse/Data/Page/Action.php b/src/Sessions/SessionReplayResponse/Data/Page/Action.php new file mode 100644 index 0000000..052e9fb --- /dev/null +++ b/src/Sessions/SessionReplayResponse/Data/Page/Action.php @@ -0,0 +1,72 @@ + */ + use SdkModel; + + #[Optional] + public ?string $method; + + #[Optional] + public ?TokenUsage $tokenUsage; + + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + * + * @param TokenUsage|TokenUsageShape|null $tokenUsage + */ + public static function with( + ?string $method = null, + TokenUsage|array|null $tokenUsage = null + ): self { + $self = new self; + + null !== $method && $self['method'] = $method; + null !== $tokenUsage && $self['tokenUsage'] = $tokenUsage; + + return $self; + } + + public function withMethod(string $method): self + { + $self = clone $this; + $self['method'] = $method; + + return $self; + } + + /** + * @param TokenUsage|TokenUsageShape $tokenUsage + */ + public function withTokenUsage(TokenUsage|array $tokenUsage): self + { + $self = clone $this; + $self['tokenUsage'] = $tokenUsage; + + return $self; + } +} diff --git a/src/Sessions/SessionReplayResponse/Data/Page/Action/TokenUsage.php b/src/Sessions/SessionReplayResponse/Data/Page/Action/TokenUsage.php new file mode 100644 index 0000000..577ceff --- /dev/null +++ b/src/Sessions/SessionReplayResponse/Data/Page/Action/TokenUsage.php @@ -0,0 +1,107 @@ + */ + use SdkModel; + + #[Optional] + public ?float $cachedInputTokens; + + #[Optional] + public ?float $inputTokens; + + #[Optional] + public ?float $outputTokens; + + #[Optional] + public ?float $reasoningTokens; + + #[Optional] + public ?float $timeMs; + + public function __construct() + { + $this->initialize(); + } + + /** + * Construct an instance from the required parameters. + * + * You must use named parameters to construct any parameters with a default value. + */ + public static function with( + ?float $cachedInputTokens = null, + ?float $inputTokens = null, + ?float $outputTokens = null, + ?float $reasoningTokens = null, + ?float $timeMs = null, + ): self { + $self = new self; + + null !== $cachedInputTokens && $self['cachedInputTokens'] = $cachedInputTokens; + null !== $inputTokens && $self['inputTokens'] = $inputTokens; + null !== $outputTokens && $self['outputTokens'] = $outputTokens; + null !== $reasoningTokens && $self['reasoningTokens'] = $reasoningTokens; + null !== $timeMs && $self['timeMs'] = $timeMs; + + return $self; + } + + public function withCachedInputTokens(float $cachedInputTokens): self + { + $self = clone $this; + $self['cachedInputTokens'] = $cachedInputTokens; + + return $self; + } + + public function withInputTokens(float $inputTokens): self + { + $self = clone $this; + $self['inputTokens'] = $inputTokens; + + return $self; + } + + public function withOutputTokens(float $outputTokens): self + { + $self = clone $this; + $self['outputTokens'] = $outputTokens; + + return $self; + } + + public function withReasoningTokens(float $reasoningTokens): self + { + $self = clone $this; + $self['reasoningTokens'] = $reasoningTokens; + + return $self; + } + + public function withTimeMs(float $timeMs): self + { + $self = clone $this; + $self['timeMs'] = $timeMs; + + return $self; + } +} diff --git a/src/Version.php b/src/Version.php index 1ee788e..34507eb 100644 --- a/src/Version.php +++ b/src/Version.php @@ -5,5 +5,5 @@ namespace Stagehand; // x-release-please-start-version -const VERSION = '3.6.0'; +const VERSION = '3.7.0'; // x-release-please-end diff --git a/tests/Services/SessionsTest.php b/tests/Services/SessionsTest.php index a582d33..79e31c9 100644 --- a/tests/Services/SessionsTest.php +++ b/tests/Services/SessionsTest.php @@ -12,6 +12,7 @@ use Stagehand\Sessions\SessionExtractResponse; use Stagehand\Sessions\SessionNavigateResponse; use Stagehand\Sessions\SessionObserveResponse; +use Stagehand\Sessions\SessionReplayResponse; use Stagehand\Sessions\SessionStartResponse; use Tests\UnsupportedMockTests; @@ -218,6 +219,21 @@ public function testObserve(): void $this->assertInstanceOf(SessionObserveResponse::class, $result); } + #[Test] + public function testReplay(): void + { + if (UnsupportedMockTests::$skip) { + $this->markTestSkipped('Prism tests are disabled'); + } + + $result = $this->client->sessions->replay( + 'c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123' + ); + + // @phpstan-ignore-next-line method.alreadyNarrowedType + $this->assertInstanceOf(SessionReplayResponse::class, $result); + } + #[Test] public function testStart(): void {