diff --git a/README.md b/README.md index 34e3b81..16e55cc 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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(); diff --git a/perfbase_extension_stubs.php b/perfbase_extension_stubs.php index b54d7f9..c9c0ba1 100644 --- a/perfbase_extension_stubs.php +++ b/perfbase_extension_stubs.php @@ -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; + } +} diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index f59a5af..d6814cd 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -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 @@ -46,4 +53,4 @@ public function setAttribute(string $key, string $value): void; * @return void */ public function reset(): void; -} \ No newline at end of file +} diff --git a/src/Extension/PerfbaseExtension.php b/src/Extension/PerfbaseExtension.php index ee59b76..b821051 100644 --- a/src/Extension/PerfbaseExtension.php +++ b/src/Extension/PerfbaseExtension.php @@ -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); @@ -54,4 +59,4 @@ public function reset(): void { perfbase_reset(); } -} \ No newline at end of file +} diff --git a/src/Http/ApiClient.php b/src/Http/ApiClient.php index 3b5644c..45cd353 100644 --- a/src/Http/ApiClient.php +++ b/src/Http/ApiClient.php @@ -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', ]; @@ -59,11 +58,13 @@ 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); } /** @@ -71,12 +72,22 @@ public function submitTrace(string $perfData): SubmitResult * * @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, ]; diff --git a/src/Perfbase.php b/src/Perfbase.php index 6bc2c59..5176909 100644 --- a/src/Perfbase.php +++ b/src/Perfbase.php @@ -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; @@ -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 @@ -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(); diff --git a/src/TracePayloadFactory.php b/src/TracePayloadFactory.php deleted file mode 100644 index a176362..0000000 --- a/src/TracePayloadFactory.php +++ /dev/null @@ -1,54 +0,0 @@ -"}. This factory - * owns the outer envelope — it validates structure, injects the client - * timestamp, and produces the final JSON body for the receiver. - */ -class TracePayloadFactory -{ - /** - * Build a submission payload from raw extension output. - * - * @param string $extensionOutput Raw JSON from perfbase_get_data() - * @return string JSON payload ready for HTTP submission - * @throws PerfbaseException If the extension output is malformed - */ - public static function build(string $extensionOutput): string - { - if ($extensionOutput === '') { - throw new PerfbaseException('Extension returned empty trace data'); - } - - /** @var mixed $decoded */ - $decoded = json_decode($extensionOutput, true); - - if (!is_array($decoded)) { - throw new PerfbaseException('Extension returned invalid JSON'); - } - - if (!isset($decoded['v']) || !is_int($decoded['v'])) { - throw new PerfbaseException('Extension output missing version field (v)'); - } - - if (!isset($decoded['p']) || !is_string($decoded['p'])) { - throw new PerfbaseException('Extension output missing payload field (p)'); - } - - // Inject client timestamp so the receiver knows when the trace was created - $decoded['d'] = gmdate('Y-m-d\TH:i:s\Z'); - - $json = json_encode($decoded); - if ($json === false) { - throw new PerfbaseException('Failed to encode submission payload'); - } - - return $json; - } -} diff --git a/src/Utils/ExtensionUtils.php b/src/Utils/ExtensionUtils.php index bcc9c2c..7821d65 100644 --- a/src/Utils/ExtensionUtils.php +++ b/src/Utils/ExtensionUtils.php @@ -12,6 +12,7 @@ class ExtensionUtils 'perfbase_disable', 'perfbase_reset', 'perfbase_get_data', + 'perfbase_get_version', 'perfbase_set_attribute' ]; diff --git a/tests/Extension/ExtensionTest.php b/tests/Extension/ExtensionTest.php index 00f5d43..fdd23b9 100644 --- a/tests/Extension/ExtensionTest.php +++ b/tests/Extension/ExtensionTest.php @@ -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 */ @@ -125,4 +142,4 @@ public function testSetAttribute(): void $this->assertTrue(true); // If we get here, no exception was thrown } -} \ No newline at end of file +} diff --git a/tests/Http/ApiClientTest.php b/tests/Http/ApiClientTest.php index cf72bcc..fe33067 100644 --- a/tests/Http/ApiClientTest.php +++ b/tests/Http/ApiClientTest.php @@ -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']); } @@ -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()); @@ -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'); @@ -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()); } @@ -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()); diff --git a/tests/Integration/PerfbaseIntegrationTest.php b/tests/Integration/PerfbaseIntegrationTest.php index f642d74..65534d2 100644 --- a/tests/Integration/PerfbaseIntegrationTest.php +++ b/tests/Integration/PerfbaseIntegrationTest.php @@ -17,8 +17,9 @@ */ class PerfbaseIntegrationTest extends BaseTest { - /** Valid extension JSON output for use in tests that call submitTrace() */ - private const EXTENSION_JSON = '{"v":1,"p":"dGVzdA=="}'; + /** Valid raw extension bytes for use in tests that call submitTrace() */ + private const EXTENSION_BYTES = 'test'; + private const ISO_8601_UTC_PATTERN = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/'; private MockInterface $mockExtension; private MockInterface $mockHttpClient; @@ -55,14 +56,13 @@ public function testCompleteProfilingWorkflow(): void $this->mockExtension->shouldReceive('isAvailable')->once()->andReturn(true); $this->mockExtension->shouldReceive('startSpan')->once()->with('integration-span', $this->config->flags, []); $this->mockExtension->shouldReceive('stopSpan')->once()->with('integration-span'); - $this->mockExtension->shouldReceive('getSpanData')->twice()->andReturn(self::EXTENSION_JSON); + $this->mockExtension->shouldReceive('getSpanData')->twice()->andReturn(self::EXTENSION_BYTES); + $this->mockExtension->shouldReceive('getVersion')->once()->andReturn(1); $this->mockExtension->shouldReceive('reset')->twice(); $this->mockApiClient->shouldReceive('submitTrace') ->once() - ->with(Mockery::on(function (string $p): bool { - return strpos($p, '"v":1') !== false; - })) + ->with(self::EXTENSION_BYTES, 1, Mockery::pattern(self::ISO_8601_UTC_PATTERN)) ->andReturn(SubmitResult::success(202)); $perfbase = new Perfbase($this->config, $this->mockExtension, $this->mockApiClient); @@ -71,7 +71,7 @@ public function testCompleteProfilingWorkflow(): void $this->assertTrue($perfbase->stopTraceSpan('integration-span')); $traceData = $perfbase->getTraceData(); - $this->assertEquals(self::EXTENSION_JSON, $traceData); + $this->assertEquals(self::EXTENSION_BYTES, $traceData); $result = $perfbase->submitTrace(); $this->assertTrue($result->isSuccess()); @@ -88,7 +88,8 @@ public function testMultipleSpansWorkflow(): void $this->mockExtension->shouldReceive('stopSpan')->once()->with('span-1'); $this->mockExtension->shouldReceive('startSpan')->once()->with('span-2', $this->config->flags, []); $this->mockExtension->shouldReceive('stopSpan')->once()->with('span-2'); - $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn(self::EXTENSION_JSON); + $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn(self::EXTENSION_BYTES); + $this->mockExtension->shouldReceive('getVersion')->once()->andReturn(1); $this->mockExtension->shouldReceive('reset')->twice(); $this->mockApiClient->shouldReceive('submitTrace') @@ -118,7 +119,8 @@ public function testWorkflowWithConfigurationChanges(): void $this->mockExtension->shouldReceive('startSpan')->once()->with('modified-span', $newFlags, []); $this->mockExtension->shouldReceive('stopSpan')->once()->with('config-span'); $this->mockExtension->shouldReceive('stopSpan')->once()->with('modified-span'); - $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn(self::EXTENSION_JSON); + $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn(self::EXTENSION_BYTES); + $this->mockExtension->shouldReceive('getVersion')->once()->andReturn(1); $this->mockExtension->shouldReceive('reset')->twice(); $this->mockApiClient->shouldReceive('submitTrace') @@ -162,22 +164,26 @@ public function testErrorHandlingInWorkflow(): void */ public function testApiClientIntegration(): void { - $testData = '{"v":1,"p":"test","d":"2024-01-01T00:00:00Z"}'; + $testData = 'binary-data'; + $clientCreatedAt = '2024-01-01T00:00:00Z'; $this->mockHttpClient->shouldReceive('post') ->once() - ->with('/v1/submit', Mockery::on(function ($options) use ($testData) { + ->with('/v1/submit', Mockery::on(function ($options) use ($testData, $clientCreatedAt) { return $options['body'] === $testData - && $options['headers']['Authorization'] === 'Bearer integration-test-key'; + && $options['headers']['Authorization'] === 'Bearer integration-test-key' + && $options['headers']['Content-Type'] === 'application/octet-stream' + && $options['headers']['X-Perfbase-Payload-Version'] === '1' + && $options['headers']['X-Perfbase-Client-Created-At'] === $clientCreatedAt; })) ->andReturn(SubmitResult::success(202)); $apiClient = new ApiClient($this->config, $this->mockHttpClient); - $this->assertTrue($apiClient->submitTrace($testData)->isSuccess()); + $this->assertTrue($apiClient->submitTrace($testData, 1, $clientCreatedAt)->isSuccess()); } /** - * Test full stack integration: Perfbase → TracePayloadFactory → ApiClient → HttpClient + * Test full stack integration: Perfbase → ApiClient → HttpClient * @covers \Perfbase\SDK\Perfbase */ public function testFullStackIntegration(): void @@ -185,18 +191,17 @@ public function testFullStackIntegration(): void $this->mockExtension->shouldReceive('isAvailable')->once()->andReturn(true); $this->mockExtension->shouldReceive('startSpan')->once()->with('full-stack', $this->config->flags, []); $this->mockExtension->shouldReceive('stopSpan')->once()->with('full-stack'); - $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn(self::EXTENSION_JSON); + $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn(self::EXTENSION_BYTES); + $this->mockExtension->shouldReceive('getVersion')->once()->andReturn(1); $this->mockExtension->shouldReceive('reset')->twice(); $this->mockHttpClient->shouldReceive('post') ->once() ->with('/v1/submit', Mockery::on(function ($options) { - $body = json_decode($options['body'], true); - // Factory should have injected 'd' timestamp - return is_array($body) - && $body['v'] === 1 - && $body['p'] === 'dGVzdA==' - && isset($body['d']) + return $options['body'] === self::EXTENSION_BYTES + && $options['headers']['Content-Type'] === 'application/octet-stream' + && $options['headers']['X-Perfbase-Payload-Version'] === '1' + && preg_match(self::ISO_8601_UTC_PATTERN, $options['headers']['X-Perfbase-Client-Created-At']) === 1 && isset($options['headers']['Authorization']); })) ->andReturn(SubmitResult::success(202)); @@ -218,7 +223,8 @@ public function testFailedSubmissionPreservesState(): void $this->mockExtension->shouldReceive('isAvailable')->once()->andReturn(true); $this->mockExtension->shouldReceive('startSpan')->once(); $this->mockExtension->shouldReceive('stopSpan')->once(); - $this->mockExtension->shouldReceive('getSpanData')->andReturn(self::EXTENSION_JSON); + $this->mockExtension->shouldReceive('getSpanData')->andReturn(self::EXTENSION_BYTES); + $this->mockExtension->shouldReceive('getVersion')->once()->andReturn(1); $this->mockExtension->shouldReceive('reset')->once(); // destructor only $this->mockHttpClient->shouldReceive('post') diff --git a/tests/PerfbaseTest.php b/tests/PerfbaseTest.php index 52cd3c5..b5669c9 100644 --- a/tests/PerfbaseTest.php +++ b/tests/PerfbaseTest.php @@ -220,19 +220,14 @@ public function testGetTraceData(): void */ public function testSubmitTraceResetsOnSuccess(): void { - $extensionJson = json_encode(['v' => 1, 'p' => 'dGVzdA==']); + $extensionData = 'test-binary-data'; $this->mockExtension->shouldReceive('isAvailable')->once()->andReturn(true); - $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn($extensionJson); + $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn($extensionData); + $this->mockExtension->shouldReceive('getVersion')->once()->andReturn(1); $this->mockExtension->shouldReceive('reset')->twice(); // Called by submitTrace and destructor $this->mockApiClient->shouldReceive('submitTrace') ->once() - ->with(Mockery::on(function (string $payload) { - $decoded = json_decode($payload, true); - return is_array($decoded) - && $decoded['v'] === 1 - && $decoded['p'] === 'dGVzdA==' - && isset($decoded['d']); // timestamp injected - })) + ->with($extensionData, 1, Mockery::pattern('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/')) ->andReturn(SubmitResult::success(202)); $perfbase = new Perfbase($this->config, $this->mockExtension, $this->mockApiClient); @@ -246,13 +241,15 @@ public function testSubmitTraceResetsOnSuccess(): void */ public function testSubmitTraceDoesNotResetOnFailure(): void { - $extensionJson = json_encode(['v' => 1, 'p' => 'dGVzdA==']); + $extensionData = 'test-binary-data'; $this->mockExtension->shouldReceive('isAvailable')->once()->andReturn(true); $this->mockExtension->shouldReceive('startSpan')->once(); - $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn($extensionJson); + $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn($extensionData); + $this->mockExtension->shouldReceive('getVersion')->once()->andReturn(1); $this->mockExtension->shouldReceive('reset')->once(); // Only destructor, NOT submitTrace $this->mockApiClient->shouldReceive('submitTrace') ->once() + ->with($extensionData, 1, Mockery::pattern('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/')) ->andReturn(SubmitResult::retryableFailure(503, 'Service Unavailable')); $perfbase = new Perfbase($this->config, $this->mockExtension, $this->mockApiClient); @@ -317,16 +314,17 @@ public function testDestructorCallsReset(): void /** * @covers ::submitTrace */ - public function testSubmitTraceThrowsWhenExtensionReturnsMalformedData(): void + public function testSubmitTraceThrowsWhenExtensionReturnsInvalidVersion(): void { $this->mockExtension->shouldReceive('isAvailable')->once()->andReturn(true); - $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn('not valid json'); + $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn('binary-data'); + $this->mockExtension->shouldReceive('getVersion')->once()->andReturn(0); $this->mockExtension->shouldReceive('reset')->once(); // destructor $perfbase = new Perfbase($this->config, $this->mockExtension, $this->mockApiClient); $this->expectException(PerfbaseException::class); - $this->expectExceptionMessage('invalid JSON'); + $this->expectExceptionMessage('invalid encoding version'); $perfbase->submitTrace(); } @@ -337,6 +335,7 @@ public function testSubmitTraceThrowsWhenExtensionReturnsEmpty(): void { $this->mockExtension->shouldReceive('isAvailable')->once()->andReturn(true); $this->mockExtension->shouldReceive('getSpanData')->once()->andReturn(''); + $this->mockExtension->shouldReceive('getVersion')->never(); $this->mockExtension->shouldReceive('reset')->once(); // destructor $perfbase = new Perfbase($this->config, $this->mockExtension, $this->mockApiClient); diff --git a/tests/TracePayloadFactoryTest.php b/tests/TracePayloadFactoryTest.php deleted file mode 100644 index 02dba71..0000000 --- a/tests/TracePayloadFactoryTest.php +++ /dev/null @@ -1,96 +0,0 @@ - 1, 'p' => 'dGVzdA==']); - - $output = TracePayloadFactory::build($input); - $decoded = json_decode($output, true); - - $this->assertSame(1, $decoded['v']); - $this->assertSame('dGVzdA==', $decoded['p']); - $this->assertArrayHasKey('d', $decoded); - $this->assertMatchesRegularExpression('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', $decoded['d']); - } - - /** - * @covers ::build - */ - public function testBuildPreservesExistingFields(): void - { - $input = json_encode(['v' => 1, 'p' => 'abc123']); - - $decoded = json_decode(TracePayloadFactory::build($input), true); - - $this->assertSame(1, $decoded['v']); - $this->assertSame('abc123', $decoded['p']); - } - - /** - * @covers ::build - */ - public function testBuildThrowsOnEmptyInput(): void - { - $this->expectException(PerfbaseException::class); - $this->expectExceptionMessage('empty trace data'); - - TracePayloadFactory::build(''); - } - - /** - * @covers ::build - */ - public function testBuildThrowsOnInvalidJson(): void - { - $this->expectException(PerfbaseException::class); - $this->expectExceptionMessage('invalid JSON'); - - TracePayloadFactory::build('not json'); - } - - /** - * @covers ::build - */ - public function testBuildThrowsOnMissingVersion(): void - { - $this->expectException(PerfbaseException::class); - $this->expectExceptionMessage('version field'); - - TracePayloadFactory::build(json_encode(['p' => 'abc'])); - } - - /** - * @covers ::build - */ - public function testBuildThrowsOnMissingPayload(): void - { - $this->expectException(PerfbaseException::class); - $this->expectExceptionMessage('payload field'); - - TracePayloadFactory::build(json_encode(['v' => 1])); - } - - /** - * @covers ::build - */ - public function testBuildOutputIsValidJson(): void - { - $input = json_encode(['v' => 1, 'p' => 'dGVzdA==']); - $output = TracePayloadFactory::build($input); - - $this->assertJson($output); - } -} diff --git a/tests/Utils/ExtensionUtilsTest.php b/tests/Utils/ExtensionUtilsTest.php index bb7f76f..5c910b1 100644 --- a/tests/Utils/ExtensionUtilsTest.php +++ b/tests/Utils/ExtensionUtilsTest.php @@ -46,6 +46,7 @@ public function testPerfbaseMethodsAvailableChecksAllRequiredMethods(): void 'perfbase_disable', 'perfbase_reset', 'perfbase_get_data', + 'perfbase_get_version', 'perfbase_set_attribute' ]; @@ -101,10 +102,11 @@ public function testMethodsArrayContainsExpectedFunctions(): void $this->assertContains('perfbase_disable', $methods); $this->assertContains('perfbase_reset', $methods); $this->assertContains('perfbase_get_data', $methods); + $this->assertContains('perfbase_get_version', $methods); $this->assertContains('perfbase_set_attribute', $methods); - // Verify the array has exactly 5 methods - $this->assertCount(5, $methods); + // Verify the array has exactly 6 methods + $this->assertCount(6, $methods); } /** @@ -146,4 +148,4 @@ public function testConsistencyOfResults(): void $this->assertEquals($methodsResult1, $methodsResult2, 'Methods available check should be consistent'); } -} \ No newline at end of file +}