From ab84b985fa9ef35cc6d6b0b64879e2efa4083c66 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Wed, 10 Dec 2025 13:32:45 +0530 Subject: [PATCH 1/3] Add telemetry feature to collect anonymous usage data Signed-off-by: Pushpak Chhajed --- config/boost.php | 16 ++ rector.php | 2 - src/BoostServiceProvider.php | 3 + src/Mcp/ToolExecutor.php | 5 + src/Telemetry/TelemetryCollector.php | 82 +++++++ .../Unit/Telemetry/TelemetryCollectorTest.php | 202 ++++++++++++++++++ 6 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 src/Telemetry/TelemetryCollector.php create mode 100644 tests/Unit/Telemetry/TelemetryCollectorTest.php diff --git a/config/boost.php b/config/boost.php index d99e4c65..2d18d098 100644 --- a/config/boost.php +++ b/config/boost.php @@ -29,4 +29,20 @@ 'browser_logs_watcher' => env('BOOST_BROWSER_LOGS_WATCHER', true), + /* + |-------------------------------------------------------------------------- + | Telemetry + |-------------------------------------------------------------------------- + | + | Boost collects anonymous usage telemetry to help improve the tool. + | Only tool names and invocation counts are collected - no file paths, + | code, or identifying information is ever sent to telemetry. + | + */ + + 'telemetry' => [ + 'enabled' => env('BOOST_TELEMETRY_ENABLED', true), + 'url' => env('BOOST_TELEMETRY_URL', 'https://boost.laravel.com/api/telemetry'), + ], + ]; diff --git a/rector.php b/rector.php index b9c1690f..5f495026 100644 --- a/rector.php +++ b/rector.php @@ -3,7 +3,6 @@ declare(strict_types=1); use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector; -use Rector\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector; use Rector\Config\RectorConfig; use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector; @@ -17,7 +16,6 @@ ReadOnlyPropertyRector::class, EncapsedStringsToSprintfRector::class, DisallowedEmptyRuleFixerRector::class, - FunctionLikeToFirstClassCallableRector::class, ]) ->withPreparedSets( deadCode: true, diff --git a/src/BoostServiceProvider.php b/src/BoostServiceProvider.php index 32df690d..30a47983 100644 --- a/src/BoostServiceProvider.php +++ b/src/BoostServiceProvider.php @@ -14,6 +14,7 @@ use Illuminate\View\Compilers\BladeCompiler; use Laravel\Boost\Mcp\Boost; use Laravel\Boost\Middleware\InjectBoost; +use Laravel\Boost\Telemetry\TelemetryCollector; use Laravel\Mcp\Facades\Mcp; use Laravel\Roster\Roster; @@ -58,6 +59,8 @@ public function register(): void return $roster; }); + + $this->app->singleton(TelemetryCollector::class); } public function boot(Router $router): void diff --git a/src/Mcp/ToolExecutor.php b/src/Mcp/ToolExecutor.php index 06076484..699cdd8c 100644 --- a/src/Mcp/ToolExecutor.php +++ b/src/Mcp/ToolExecutor.php @@ -6,6 +6,7 @@ use Dotenv\Dotenv; use Illuminate\Support\Env; +use Laravel\Boost\Telemetry\TelemetryCollector; use Laravel\Mcp\Response; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; @@ -19,6 +20,10 @@ public function execute(string $toolClass, array $arguments = []): Response return Response::error("Tool not registered or not allowed: {$toolClass}"); } + if (config('boost.telemetry.enabled')) { + app(TelemetryCollector::class)->record($toolClass); + } + return $this->executeInSubprocess($toolClass, $arguments); } diff --git a/src/Telemetry/TelemetryCollector.php b/src/Telemetry/TelemetryCollector.php new file mode 100644 index 00000000..8f617df1 --- /dev/null +++ b/src/Telemetry/TelemetryCollector.php @@ -0,0 +1,82 @@ +toolCounts); + if ($totalCount >= self::MAX_TOOLS_PER_FLUSH) { + $this->flush(); + } + + if (! $this->shutdownRegistered) { + if (extension_loaded('pcntl')) { + pcntl_async_signals(true); + pcntl_signal(SIGINT, $this->flush(...)); + pcntl_signal(SIGTERM, $this->flush(...)); + } + + register_shutdown_function([$this, 'flush']); + + app()->terminating($this->flush(...)); + + $this->shutdownRegistered = true; + } + + $this->toolCounts[$toolName] = ($this->toolCounts[$toolName] ?? 0) + 1; + } + + public function flush(): void + { + if ($this->toolCounts === [] || ! config('boost.telemetry.enabled', true)) { + return; + } + + try { + Http::timeout(5) + ->withHeaders(['User-Agent' => 'Laravel Boost Telemetry']) + ->post(config('boost.telemetry.url'), ['data' => $this->buildPayload()]); + } catch (Throwable) { + // + } finally { + $this->toolCounts = []; + } + + } + + protected function buildPayload(): string + { + $version = InstalledVersions::getVersion('laravel/boost'); + + return base64_encode(json_encode([ + 'session_id' => hash('sha256', base_path()), + 'boost_version' => $version, + 'php_version' => PHP_VERSION, + 'os' => PHP_OS_FAMILY, + 'laravel_version' => app()->version(), + 'tools' => $this->toolCounts, + 'timestamp' => now()->toIso8601String(), + ])); + } +} diff --git a/tests/Unit/Telemetry/TelemetryCollectorTest.php b/tests/Unit/Telemetry/TelemetryCollectorTest.php new file mode 100644 index 00000000..1f0d0c4c --- /dev/null +++ b/tests/Unit/Telemetry/TelemetryCollectorTest.php @@ -0,0 +1,202 @@ +collector = app(TelemetryCollector::class); + $this->collector->toolCounts = []; +}); + +it('records tool invocations', function (): void { + config(['boost.telemetry.enabled' => true]); + + $this->collector->record(DatabaseQuery::class); + $this->collector->record(DatabaseQuery::class); + $this->collector->record(Tinker::class); + + expect($this->collector->toolCounts)->toBe([ + DatabaseQuery::class => 2, + Tinker::class => 1, + ]); +}); + +it('does not record when disabled via config', function (): void { + config(['boost.telemetry.enabled' => false]); + + $this->collector->record(DatabaseQuery::class); + + expect($this->collector->toolCounts)->toBe([]); +}); + +it('auto-flushes when reaching MAX_TOOLS_PER_FLUSH', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + for ($i = 0; $i < 20; $i++) { + $this->collector->record(Tinker::class); + } + + expect($this->collector->toolCounts)->toHaveCount(1) + ->and($this->collector->toolCounts[Tinker::class])->toBe(20); + + $this->collector->record(Tinker::class); + + expect(Http::recorded())->toHaveCount(1) + ->and($this->collector->toolCounts)->toHaveCount(1) + ->and($this->collector->toolCounts[Tinker::class])->toBe(1); +}); + +it('does not auto-flush below MAX_TOOLS_PER_FLUSH', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + for ($i = 0; $i < 19; $i++) { + $this->collector->record(Tinker::class); + } + + expect(Http::recorded())->toHaveCount(0) + ->and($this->collector->toolCounts)->toHaveCount(1) + ->and($this->collector->toolCounts[Tinker::class])->toBe(19); +}); + +it('flush sends data and clears counts', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + $this->collector->record(Tinker::class); + $this->collector->flush(); + + expect(Http::recorded())->toHaveCount(1); + + $request = Http::recorded()[0][0]; + $payload = json_decode(base64_decode((string) $request['data'], true), true); + + expect($request->url())->toBe(config('boost.telemetry.url')) + ->and($payload['tools'][Tinker::class])->toBe(1) + ->and($this->collector->toolCounts)->toBe([]); +}); + +it('flush does nothing when toolCounts is empty', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + $this->collector->flush(); + + expect(Http::recorded())->toHaveCount(0); +}); + +it('flush does nothing when telemetry is disabled', function (): void { + config(['boost.telemetry.enabled' => false]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + $this->collector->toolCounts = ['SomeTool' => 1]; + $this->collector->flush(); + + expect(Http::recorded())->toHaveCount(0); +}); + +it('flush fails silently on network error', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(null, 500), + ]); + + $this->collector->record(Tinker::class); + $this->collector->flush(); + + expect($this->collector->toolCounts)->toBe([]); +}); + +it('flush fails silently on connection timeout', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake(function (): void { + throw new \Exception('Connection timeout'); + }); + + $this->collector->record(Tinker::class); + $this->collector->flush(); + + expect($this->collector->toolCounts)->toBe([]); +}); + +it('includes buildPayload as the correct structure', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + $this->collector->record(Tinker::class); + $this->collector->flush(); + + expect(Http::recorded())->toHaveCount(1); + + $request = Http::recorded()[0][0]; + $payload = json_decode(base64_decode((string) $request['data'], true), true); + + expect($payload)->toHaveKeys(['session_id', 'boost_version', 'php_version', 'os', 'laravel_version', 'tools', 'timestamp']) + ->and($payload['php_version'])->toBe(PHP_VERSION) + ->and($payload['os'])->toBe(PHP_OS_FAMILY) + ->and($payload['tools'])->toBeArray(); +}); + +it('sends session_id as a consistent hash of base_path', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + $expectedSessionId = hash('sha256', base_path()); + + $this->collector->record(Tinker::class); + $this->collector->flush(); + + expect(Http::recorded())->toHaveCount(1); + + $request = Http::recorded()[0][0]; + $payload = json_decode(base64_decode((string) $request['data'], true), true); + + expect($payload['session_id'])->toBe($expectedSessionId); +}); + +it('uses boost_version as InstalledVersions', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + $expectedVersion = InstalledVersions::getVersion('laravel/boost'); + + $this->collector->record(Tinker::class); + $this->collector->flush(); + + expect(Http::recorded())->toHaveCount(1); + + $request = Http::recorded()[0][0]; + $payload = json_decode(base64_decode((string) $request['data'], true), true); + + expect($payload['boost_version'])->toBe($expectedVersion); +}); From 4be69000d07e0b523dcb1c71d5eae24e251b2d07 Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Wed, 10 Dec 2025 16:15:03 +0530 Subject: [PATCH 2/3] Fix Test Signed-off-by: Pushpak Chhajed --- src/Telemetry/TelemetryCollector.php | 64 +++++++++++-------- .../Unit/Telemetry/TelemetryCollectorTest.php | 10 +-- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/Telemetry/TelemetryCollector.php b/src/Telemetry/TelemetryCollector.php index 8f617df1..c1f85514 100644 --- a/src/Telemetry/TelemetryCollector.php +++ b/src/Telemetry/TelemetryCollector.php @@ -4,44 +4,57 @@ namespace Laravel\Boost\Telemetry; -use const PHP_OS_FAMILY; -use const PHP_VERSION; - use Composer\InstalledVersions; -use Illuminate\Support\Facades\Http; +use Laravel\Boost\Concerns\MakesHttpRequests; use Throwable; class TelemetryCollector { + use MakesHttpRequests; + protected const MAX_TOOLS_PER_FLUSH = 20; public array $toolCounts = []; - protected bool $shutdownRegistered = false; + protected bool $enabled; - public function record(string $toolName): void - { - if (! config('boost.telemetry.enabled')) { - return; - } + protected string $url; - $totalCount = array_sum($this->toolCounts); - if ($totalCount >= self::MAX_TOOLS_PER_FLUSH) { - $this->flush(); - } + protected string $sessionId; + + protected string $laravelVersion; + + public function __construct() + { + $this->enabled = config('boost.telemetry.enabled', false); + if ($this->enabled) { + $this->url = config('boost.telemetry.url', 'https://boost.laravel.com/api/telemetry'); + $this->sessionId = hash('sha256', base_path()); + $this->laravelVersion = app()->version(); + app()->terminating($this->flush(...)); - if (! $this->shutdownRegistered) { if (extension_loaded('pcntl')) { pcntl_async_signals(true); pcntl_signal(SIGINT, $this->flush(...)); pcntl_signal(SIGTERM, $this->flush(...)); } + } + } - register_shutdown_function([$this, 'flush']); + public function __destruct() + { + $this->flush(); + } - app()->terminating($this->flush(...)); + public function record(string $toolName): void + { + if (! $this->enabled) { + return; + } - $this->shutdownRegistered = true; + $totalCount = array_sum($this->toolCounts); + if ($totalCount >= self::MAX_TOOLS_PER_FLUSH) { + $this->flush(); } $this->toolCounts[$toolName] = ($this->toolCounts[$toolName] ?? 0) + 1; @@ -49,20 +62,19 @@ public function record(string $toolName): void public function flush(): void { - if ($this->toolCounts === [] || ! config('boost.telemetry.enabled', true)) { + if ($this->toolCounts === [] || ! $this->enabled) { return; } try { - Http::timeout(5) - ->withHeaders(['User-Agent' => 'Laravel Boost Telemetry']) - ->post(config('boost.telemetry.url'), ['data' => $this->buildPayload()]); + $this->client() + ->timeout(5) + ->post($this->url, ['data' => $this->buildPayload()]); } catch (Throwable) { // } finally { $this->toolCounts = []; } - } protected function buildPayload(): string @@ -70,13 +82,13 @@ protected function buildPayload(): string $version = InstalledVersions::getVersion('laravel/boost'); return base64_encode(json_encode([ - 'session_id' => hash('sha256', base_path()), + 'session_id' => $this->sessionId, 'boost_version' => $version, 'php_version' => PHP_VERSION, 'os' => PHP_OS_FAMILY, - 'laravel_version' => app()->version(), + 'laravel_version' => $this->laravelVersion, 'tools' => $this->toolCounts, - 'timestamp' => now()->toIso8601String(), + 'timestamp' => date('c'), ])); } } diff --git a/tests/Unit/Telemetry/TelemetryCollectorTest.php b/tests/Unit/Telemetry/TelemetryCollectorTest.php index 1f0d0c4c..a6c80400 100644 --- a/tests/Unit/Telemetry/TelemetryCollectorTest.php +++ b/tests/Unit/Telemetry/TelemetryCollectorTest.php @@ -27,9 +27,10 @@ it('does not record when disabled via config', function (): void { config(['boost.telemetry.enabled' => false]); - $this->collector->record(DatabaseQuery::class); + $collector = new TelemetryCollector; + $collector->record(DatabaseQuery::class); - expect($this->collector->toolCounts)->toBe([]); + expect($collector->toolCounts)->toBe([]); }); it('auto-flushes when reaching MAX_TOOLS_PER_FLUSH', function (): void { @@ -108,8 +109,9 @@ '*' => Http::response(['status' => 'ok'], 200), ]); - $this->collector->toolCounts = ['SomeTool' => 1]; - $this->collector->flush(); + $collector = new TelemetryCollector; + $collector->toolCounts = ['SomeTool' => 1]; + $collector->flush(); expect(Http::recorded())->toHaveCount(0); }); From af35c2d6c12e6022beb953809bd87a55d7f2ee9d Mon Sep 17 00:00:00 2001 From: Pushpak Chhajed Date: Wed, 10 Dec 2025 19:34:29 +0530 Subject: [PATCH 3/3] Add more data Signed-off-by: Pushpak Chhajed --- src/Mcp/ToolExecutor.php | 11 +- src/Telemetry/TelemetryCollector.php | 46 +++++-- .../Unit/Telemetry/TelemetryCollectorTest.php | 125 +++++++++--------- 3 files changed, 106 insertions(+), 76 deletions(-) diff --git a/src/Mcp/ToolExecutor.php b/src/Mcp/ToolExecutor.php index 699cdd8c..d4f767f5 100644 --- a/src/Mcp/ToolExecutor.php +++ b/src/Mcp/ToolExecutor.php @@ -20,10 +20,6 @@ public function execute(string $toolClass, array $arguments = []): Response return Response::error("Tool not registered or not allowed: {$toolClass}"); } - if (config('boost.telemetry.enabled')) { - app(TelemetryCollector::class)->record($toolClass); - } - return $this->executeInSubprocess($toolClass, $arguments); } @@ -46,11 +42,14 @@ protected function executeInSubprocess(string $toolClass, array $arguments): Res timeout: $this->getTimeout($arguments) ); + $wordCount = 0; + try { $process->mustRun(); $output = $process->getOutput(); $decoded = json_decode($output, true); + $wordCount = str_word_count($output); if (json_last_error() !== JSON_ERROR_NONE) { return Response::error('Invalid JSON output from tool process: '.json_last_error_msg()); @@ -66,6 +65,10 @@ protected function executeInSubprocess(string $toolClass, array $arguments): Res $errorOutput = $process->getErrorOutput().$process->getOutput(); return Response::error("Process tool execution failed: {$errorOutput}"); + } finally { + if (config('boost.telemetry.enabled')) { + app(TelemetryCollector::class)->record($toolClass, $wordCount); + } } } diff --git a/src/Telemetry/TelemetryCollector.php b/src/Telemetry/TelemetryCollector.php index c1f85514..cf191fdb 100644 --- a/src/Telemetry/TelemetryCollector.php +++ b/src/Telemetry/TelemetryCollector.php @@ -12,9 +12,7 @@ class TelemetryCollector { use MakesHttpRequests; - protected const MAX_TOOLS_PER_FLUSH = 20; - - public array $toolCounts = []; + public array $toolData = []; protected bool $enabled; @@ -24,8 +22,11 @@ class TelemetryCollector protected string $laravelVersion; + protected float $sessionStartTime; + public function __construct() { + $this->sessionStartTime = microtime(true); $this->enabled = config('boost.telemetry.enabled', false); if ($this->enabled) { $this->url = config('boost.telemetry.url', 'https://boost.laravel.com/api/telemetry'); @@ -46,23 +47,28 @@ public function __destruct() $this->flush(); } - public function record(string $toolName): void + public function record(string $toolName, int $wordCount): void { if (! $this->enabled) { return; } - $totalCount = array_sum($this->toolCounts); - if ($totalCount >= self::MAX_TOOLS_PER_FLUSH) { - $this->flush(); + if (! isset($this->toolData[$toolName])) { + $this->toolData[$toolName] = []; } - $this->toolCounts[$toolName] = ($this->toolCounts[$toolName] ?? 0) + 1; + $tokens = $this->calculateTokens($wordCount); + $this->toolData[$toolName][] = ['tokens' => $tokens]; + } + + protected function calculateTokens(int $wordCount): int + { + return (int) round($wordCount * 1.3); } public function flush(): void { - if ($this->toolCounts === [] || ! $this->enabled) { + if ($this->toolData === [] || ! $this->enabled) { return; } @@ -73,13 +79,15 @@ public function flush(): void } catch (Throwable) { // } finally { - $this->toolCounts = []; + $this->toolData = []; + $this->sessionStartTime = microtime(true); } } protected function buildPayload(): string { $version = InstalledVersions::getVersion('laravel/boost'); + $sessionEndTime = microtime(true); return base64_encode(json_encode([ 'session_id' => $this->sessionId, @@ -87,8 +95,24 @@ protected function buildPayload(): string 'php_version' => PHP_VERSION, 'os' => PHP_OS_FAMILY, 'laravel_version' => $this->laravelVersion, - 'tools' => $this->toolCounts, + 'session_start' => date('c', (int) $this->sessionStartTime), + 'session_end' => date('c', (int) $sessionEndTime), + 'tools' => $this->formatToolsData(), 'timestamp' => date('c'), ])); } + + protected function formatToolsData(): array + { + $formatted = []; + + foreach ($this->toolData as $toolName => $invocations) { + $formatted[$toolName] = []; + foreach ($invocations as $index => $invocation) { + $formatted[$toolName][(string) ($index + 1)] = $invocation; + } + } + + return $formatted; + } } diff --git a/tests/Unit/Telemetry/TelemetryCollectorTest.php b/tests/Unit/Telemetry/TelemetryCollectorTest.php index a6c80400..bee6857e 100644 --- a/tests/Unit/Telemetry/TelemetryCollectorTest.php +++ b/tests/Unit/Telemetry/TelemetryCollectorTest.php @@ -8,19 +8,24 @@ beforeEach(function (): void { $this->collector = app(TelemetryCollector::class); - $this->collector->toolCounts = []; + $this->collector->toolData = []; }); it('records tool invocations', function (): void { config(['boost.telemetry.enabled' => true]); - $this->collector->record(DatabaseQuery::class); - $this->collector->record(DatabaseQuery::class); - $this->collector->record(Tinker::class); - - expect($this->collector->toolCounts)->toBe([ - DatabaseQuery::class => 2, - Tinker::class => 1, + $this->collector->record(DatabaseQuery::class, 100); + $this->collector->record(DatabaseQuery::class, 200); + $this->collector->record(Tinker::class, 150); + + expect($this->collector->toolData)->toBe([ + DatabaseQuery::class => [ + ['tokens' => 130], // 100 * 1.3 + ['tokens' => 260], // 200 * 1.3 + ], + Tinker::class => [ + ['tokens' => 195], // 150 * 1.3 + ], ]); }); @@ -28,46 +33,9 @@ config(['boost.telemetry.enabled' => false]); $collector = new TelemetryCollector; - $collector->record(DatabaseQuery::class); + $collector->record(DatabaseQuery::class, 100); - expect($collector->toolCounts)->toBe([]); -}); - -it('auto-flushes when reaching MAX_TOOLS_PER_FLUSH', function (): void { - config(['boost.telemetry.enabled' => true]); - - Http::fake([ - '*' => Http::response(['status' => 'ok'], 200), - ]); - - for ($i = 0; $i < 20; $i++) { - $this->collector->record(Tinker::class); - } - - expect($this->collector->toolCounts)->toHaveCount(1) - ->and($this->collector->toolCounts[Tinker::class])->toBe(20); - - $this->collector->record(Tinker::class); - - expect(Http::recorded())->toHaveCount(1) - ->and($this->collector->toolCounts)->toHaveCount(1) - ->and($this->collector->toolCounts[Tinker::class])->toBe(1); -}); - -it('does not auto-flush below MAX_TOOLS_PER_FLUSH', function (): void { - config(['boost.telemetry.enabled' => true]); - - Http::fake([ - '*' => Http::response(['status' => 'ok'], 200), - ]); - - for ($i = 0; $i < 19; $i++) { - $this->collector->record(Tinker::class); - } - - expect(Http::recorded())->toHaveCount(0) - ->and($this->collector->toolCounts)->toHaveCount(1) - ->and($this->collector->toolCounts[Tinker::class])->toBe(19); + expect($collector->toolData)->toBe([]); }); it('flush sends data and clears counts', function (): void { @@ -77,7 +45,7 @@ '*' => Http::response(['status' => 'ok'], 200), ]); - $this->collector->record(Tinker::class); + $this->collector->record(Tinker::class, 150); $this->collector->flush(); expect(Http::recorded())->toHaveCount(1); @@ -86,11 +54,11 @@ $payload = json_decode(base64_decode((string) $request['data'], true), true); expect($request->url())->toBe(config('boost.telemetry.url')) - ->and($payload['tools'][Tinker::class])->toBe(1) - ->and($this->collector->toolCounts)->toBe([]); + ->and($payload['tools'][Tinker::class]['1'])->toBe(['tokens' => 195]) // 150 * 1.3 + ->and($this->collector->toolData)->toBe([]); }); -it('flush does nothing when toolCounts is empty', function (): void { +it('flush does nothing when toolData is empty', function (): void { config(['boost.telemetry.enabled' => true]); Http::fake([ @@ -110,7 +78,7 @@ ]); $collector = new TelemetryCollector; - $collector->toolCounts = ['SomeTool' => 1]; + $collector->toolData = ['SomeTool' => [['tokens' => 100]]]; $collector->flush(); expect(Http::recorded())->toHaveCount(0); @@ -123,10 +91,10 @@ '*' => Http::response(null, 500), ]); - $this->collector->record(Tinker::class); + $this->collector->record(Tinker::class, 100); $this->collector->flush(); - expect($this->collector->toolCounts)->toBe([]); + expect($this->collector->toolData)->toBe([]); }); it('flush fails silently on connection timeout', function (): void { @@ -136,10 +104,10 @@ throw new \Exception('Connection timeout'); }); - $this->collector->record(Tinker::class); + $this->collector->record(Tinker::class, 100); $this->collector->flush(); - expect($this->collector->toolCounts)->toBe([]); + expect($this->collector->toolData)->toBe([]); }); it('includes buildPayload as the correct structure', function (): void { @@ -149,7 +117,7 @@ '*' => Http::response(['status' => 'ok'], 200), ]); - $this->collector->record(Tinker::class); + $this->collector->record(Tinker::class, 100); $this->collector->flush(); expect(Http::recorded())->toHaveCount(1); @@ -157,10 +125,23 @@ $request = Http::recorded()[0][0]; $payload = json_decode(base64_decode((string) $request['data'], true), true); - expect($payload)->toHaveKeys(['session_id', 'boost_version', 'php_version', 'os', 'laravel_version', 'tools', 'timestamp']) + expect($payload)->toHaveKeys([ + 'session_id', + 'boost_version', + 'php_version', + 'os', + 'laravel_version', + 'session_start', + 'session_end', + 'tools', + 'timestamp', + ]) ->and($payload['php_version'])->toBe(PHP_VERSION) ->and($payload['os'])->toBe(PHP_OS_FAMILY) - ->and($payload['tools'])->toBeArray(); + ->and($payload['tools'])->toBeArray() + ->and($payload['tools'][Tinker::class]['1']['tokens'])->toBe(130) // 100 * 1.3 + ->and(strtotime((string) $payload['session_start']))->not->toBeFalse() + ->and(strtotime((string) $payload['session_end']))->not->toBeFalse(); }); it('sends session_id as a consistent hash of base_path', function (): void { @@ -172,7 +153,7 @@ $expectedSessionId = hash('sha256', base_path()); - $this->collector->record(Tinker::class); + $this->collector->record(Tinker::class, 100); $this->collector->flush(); expect(Http::recorded())->toHaveCount(1); @@ -183,6 +164,28 @@ expect($payload['session_id'])->toBe($expectedSessionId); }); +it('records tool response sizes and resets after flush', function (): void { + config(['boost.telemetry.enabled' => true]); + + Http::fake([ + '*' => Http::response(['status' => 'ok'], 200), + ]); + + $this->collector->record(Tinker::class, 128); + $this->collector->record(Tinker::class, 256); + + $this->collector->flush(); + + expect(Http::recorded())->toHaveCount(1); + + $request = Http::recorded()[0][0]; + $payload = json_decode(base64_decode((string) $request['data'], true), true); + + expect($payload['tools'][Tinker::class]['1']['tokens'])->toBe(166) // 128 * 1.3 + ->and($payload['tools'][Tinker::class]['2']['tokens'])->toBe(333) // 256 * 1.3 + ->and($this->collector->toolData)->toBe([]); +}); + it('uses boost_version as InstalledVersions', function (): void { config(['boost.telemetry.enabled' => true]); @@ -192,7 +195,7 @@ $expectedVersion = InstalledVersions::getVersion('laravel/boost'); - $this->collector->record(Tinker::class); + $this->collector->record(Tinker::class, 100); $this->collector->flush(); expect(Http::recorded())->toHaveCount(1);