Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ $perfbase->setAttribute('cache_hit_ratio', '0.85');
#### `submitTrace(): SubmitResult`
Submit collected profiling data to Perfbase. Resets the session on success; preserves trace state on failure so callers can decide whether to retry or discard.

The SDK sends the exact raw bytes returned by `perfbase_get_data()` as the request body. It adds the payload encoding version and client-created timestamp as HTTP headers; it does not JSON-wrap or base64-encode the trace payload.

The returned `SubmitResult` provides:
- `isSuccess(): bool` — delivery confirmed
- `isRetryable(): bool` — transient failure (network error, 429, 5xx)
Expand All @@ -303,7 +305,7 @@ if (!$result->isSuccess()) {
```

#### `getTraceData(string $spanName = ''): string`
Retrieve raw trace data (useful for debugging or custom processing).
Retrieve the raw Brotli-compressed MessagePack trace payload produced by the extension (useful for debugging or custom processing).

```php
$rawData = $perfbase->getTraceData();
Expand Down
13 changes: 13 additions & 0 deletions perfbase_extension_stubs.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,16 @@ function perfbase_get_data(): string
return '';
}
}

if (!function_exists('perfbase_get_version')) {
/**
* Retrieves the encoding version for collected profiling data.
*
* @return int
*/
function perfbase_get_version(): int
{
// Stub only—no implementation needed
return 1;
}
}
9 changes: 8 additions & 1 deletion src/Extension/ExtensionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ public function stopSpan(string $spanName): void;
*/
public function getSpanData(string $spanName = ''): string;

/**
* Retrieves the encoding version for the current trace payload format.
*
* @return int
*/
public function getVersion(): int;

/**
* Sets an attribute for a specific span
* @param string $key
Expand All @@ -46,4 +53,4 @@ public function setAttribute(string $key, string $value): void;
* @return void
*/
public function reset(): void;
}
}
7 changes: 6 additions & 1 deletion src/Extension/PerfbaseExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ public function getSpanData(string $spanName = ''): string
return perfbase_get_data();
}

public function getVersion(): int
{
return perfbase_get_version();
}

public function setAttribute(string $key, string $value): void
{
perfbase_set_attribute($key, $value);
Expand All @@ -54,4 +59,4 @@ public function reset(): void
{
perfbase_reset();
}
}
}
21 changes: 16 additions & 5 deletions src/Http/ApiClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public function __construct(Config $config, ?HttpClientInterface $httpClient = n
'Authorization' => 'Bearer ' . $this->config->api_key,
'Accept' => 'application/json',
'User-Agent' => sprintf('Perfbase-PHP-SDK/%s', Perfbase::VERSION),
'Content-Type' => 'application/json',
'Connection' => 'keep-alive',
];

Expand All @@ -59,24 +58,36 @@ public function __construct(Config $config, ?HttpClientInterface $httpClient = n
* Submits a trace to the Perfbase API
*
* @param string $perfData Data to send in the request body
* @param int $payloadVersion Payload encoding version. Defaults to `1` for backwards compatibility.
* @param string|null $clientCreatedAt Trace creation timestamp in ISO 8601 UTC
* @return SubmitResult
*/
public function submitTrace(string $perfData): SubmitResult
public function submitTrace(string $perfData, int $payloadVersion = 1, ?string $clientCreatedAt = null): SubmitResult
{
return $this->submit('/v1/submit', $perfData);
return $this->submit('/v1/submit', $perfData, $payloadVersion, $clientCreatedAt);
}

/**
* Sends a POST request to the specified API endpoint
*
* @param string $endpoint API endpoint to send the request to
* @param string $perfData Data to send in the request body
* @param int $payloadVersion Payload encoding version
* @param string|null $clientCreatedAt Trace creation timestamp in ISO 8601 UTC
* @return SubmitResult
*/
private function submit(string $endpoint, string $perfData): SubmitResult
private function submit(string $endpoint, string $perfData, int $payloadVersion, ?string $clientCreatedAt = null): SubmitResult
{
$headers = $this->defaultHeaders;
$headers['Content-Type'] = 'application/octet-stream';
$headers['X-Perfbase-Payload-Version'] = (string) $payloadVersion;

if ($clientCreatedAt !== null) {
$headers['X-Perfbase-Client-Created-At'] = $clientCreatedAt;
}

$options = [
'headers' => $this->defaultHeaders,
'headers' => $headers,
'body' => $perfData,
];

Expand Down
20 changes: 17 additions & 3 deletions src/Perfbase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Perfbase\SDK;

use Perfbase\SDK\Exception\PerfbaseExtensionException;
use Perfbase\SDK\Exception\PerfbaseException;
use Perfbase\SDK\Exception\PerfbaseInvalidConfigException;
use Perfbase\SDK\Exception\PerfbaseInvalidSpanException;
use Perfbase\SDK\Extension\ExtensionInterface;
Expand All @@ -24,7 +25,7 @@ class Perfbase
/**
* The version of the Perfbase SDK
*/
public const VERSION = '1.0.0';
public const VERSION = '1.1.0';

/**
* The default span name used when starting a profiling session
Expand Down Expand Up @@ -167,9 +168,22 @@ public function setFlags(int $flags): void
*/
public function submitTrace(): SubmitResult
{
$payload = TracePayloadFactory::build($this->getTraceData());
$traceData = $this->getTraceData();

$result = $this->apiClient->submitTrace($payload);
if ($traceData === '') {
throw new PerfbaseException('Extension returned empty trace data');
}

$payloadVersion = $this->extension->getVersion();
if ($payloadVersion <= 0) {
throw new PerfbaseException('Extension returned invalid encoding version');
}

$result = $this->apiClient->submitTrace(
$traceData,
$payloadVersion,
gmdate('Y-m-d\TH:i:s\Z')
);

if ($result->isSuccess()) {
$this->reset();
Expand Down
54 changes: 0 additions & 54 deletions src/TracePayloadFactory.php

This file was deleted.

1 change: 1 addition & 0 deletions src/Utils/ExtensionUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ExtensionUtils
'perfbase_disable',
'perfbase_reset',
'perfbase_get_data',
'perfbase_get_version',
'perfbase_set_attribute'
];

Expand Down
19 changes: 18 additions & 1 deletion tests/Extension/ExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ public function testGetData(): void
$this->assertIsString($data);
}

/**
* @covers ::getVersion
*/
public function testGetVersion(): void
{
if (!ExtensionUtils::perfbaseExtensionLoaded()) {
$this->markTestSkipped('Perfbase extension not loaded');
}

$extension = new PerfbaseExtension();

$version = $extension->getVersion();

$this->assertIsInt($version);
$this->assertGreaterThan(0, $version);
}

/**
* @covers ::reset
*/
Expand Down Expand Up @@ -125,4 +142,4 @@ public function testSetAttribute(): void

$this->assertTrue(true); // If we get here, no exception was thrown
}
}
}
47 changes: 39 additions & 8 deletions tests/Http/ApiClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ public function testConstructorSetsCorrectHeaders(): void

$this->assertEquals('Bearer test-api-key', $headers['Authorization']);
$this->assertEquals('application/json', $headers['Accept']);
$this->assertEquals('application/json', $headers['Content-Type']);
$this->assertEquals('keep-alive', $headers['Connection']);
$this->assertStringContainsString('Perfbase-PHP-SDK/', $headers['User-Agent']);
}
Expand All @@ -85,12 +84,13 @@ public function testSubmitTraceReturnsResult(): void
->once()
->with('/v1/submit', Mockery::on(function ($options) use ($testData) {
return isset($options['body']) && $options['body'] === $testData
&& isset($options['headers']) && is_array($options['headers']);
&& isset($options['headers']) && is_array($options['headers'])
&& $options['headers']['X-Perfbase-Payload-Version'] === '1';
}))
->andReturn($expectedResult);

$apiClient = new ApiClient($this->config, $this->mockHttpClient);
$result = $apiClient->submitTrace($testData);
$result = $apiClient->submitTrace($testData, 1);

$this->assertTrue($result->isSuccess());
$this->assertSame(202, $result->getStatusCode());
Expand All @@ -102,18 +102,46 @@ public function testSubmitTraceReturnsResult(): void
*/
public function testSubmitTracePassesCorrectHeaders(): void
{
$clientCreatedAt = '2026-04-08T12:00:00Z';

$this->mockHttpClient->shouldReceive('post')
->once()
->with('/v1/submit', Mockery::on(function ($options) {
->with('/v1/submit', Mockery::on(function ($options) use ($clientCreatedAt) {
$headers = $options['headers'];
return $headers['Authorization'] === 'Bearer test-api-key'
&& $headers['Accept'] === 'application/json'
&& $headers['Content-Type'] === 'application/json'
&& $headers['Content-Type'] === 'application/octet-stream'
&& $headers['Connection'] === 'keep-alive'
&& $headers['X-Perfbase-Payload-Version'] === '1'
&& $headers['X-Perfbase-Client-Created-At'] === $clientCreatedAt
&& isset($headers['User-Agent']);
}))
->andReturn(SubmitResult::success());

$apiClient = new ApiClient($this->config, $this->mockHttpClient);
$result = $apiClient->submitTrace('test-data', 1, $clientCreatedAt);

$this->assertTrue($result->isSuccess());
}

/**
* @covers ::submitTrace
* @covers ::submit
*/
public function testSubmitTraceMaintainsSingleArgumentCompatibility(): void
{
$this->mockHttpClient->shouldReceive('post')
->once()
->with('/v1/submit', Mockery::on(function ($options) {
$headers = $options['headers'];

return $options['body'] === 'test-data'
&& $headers['Content-Type'] === 'application/octet-stream'
&& $headers['X-Perfbase-Payload-Version'] === '1'
&& !isset($headers['X-Perfbase-Client-Created-At']);
}))
->andReturn(SubmitResult::success());

$apiClient = new ApiClient($this->config, $this->mockHttpClient);
$result = $apiClient->submitTrace('test-data');

Expand All @@ -129,12 +157,15 @@ public function testSubmitTraceWithEmptyData(): void
$this->mockHttpClient->shouldReceive('post')
->once()
->with('/v1/submit', Mockery::on(function ($options) {
return $options['body'] === '';
return $options['body'] === ''
&& $options['headers']['Content-Type'] === 'application/octet-stream'
&& $options['headers']['X-Perfbase-Payload-Version'] === '1'
&& !isset($options['headers']['X-Perfbase-Client-Created-At']);
}))
->andReturn(SubmitResult::success());

$apiClient = new ApiClient($this->config, $this->mockHttpClient);
$result = $apiClient->submitTrace('');
$result = $apiClient->submitTrace('', 1);

$this->assertTrue($result->isSuccess());
}
Expand All @@ -151,7 +182,7 @@ public function testSubmitTracePropagateshFailureResult(): void
->andReturn($failureResult);

$apiClient = new ApiClient($this->config, $this->mockHttpClient);
$result = $apiClient->submitTrace('test-data');
$result = $apiClient->submitTrace('test-data', 1);

$this->assertTrue($result->isRetryable());
$this->assertSame(503, $result->getStatusCode());
Expand Down
Loading
Loading