Skip to content

Commit af35c2d

Browse files
committed
Add more data
Signed-off-by: Pushpak Chhajed <[email protected]>
1 parent 4be6900 commit af35c2d

File tree

3 files changed

+106
-76
lines changed

3 files changed

+106
-76
lines changed

src/Mcp/ToolExecutor.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ public function execute(string $toolClass, array $arguments = []): Response
2020
return Response::error("Tool not registered or not allowed: {$toolClass}");
2121
}
2222

23-
if (config('boost.telemetry.enabled')) {
24-
app(TelemetryCollector::class)->record($toolClass);
25-
}
26-
2723
return $this->executeInSubprocess($toolClass, $arguments);
2824
}
2925

@@ -46,11 +42,14 @@ protected function executeInSubprocess(string $toolClass, array $arguments): Res
4642
timeout: $this->getTimeout($arguments)
4743
);
4844

45+
$wordCount = 0;
46+
4947
try {
5048
$process->mustRun();
5149

5250
$output = $process->getOutput();
5351
$decoded = json_decode($output, true);
52+
$wordCount = str_word_count($output);
5453

5554
if (json_last_error() !== JSON_ERROR_NONE) {
5655
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
6665
$errorOutput = $process->getErrorOutput().$process->getOutput();
6766

6867
return Response::error("Process tool execution failed: {$errorOutput}");
68+
} finally {
69+
if (config('boost.telemetry.enabled')) {
70+
app(TelemetryCollector::class)->record($toolClass, $wordCount);
71+
}
6972
}
7073
}
7174

src/Telemetry/TelemetryCollector.php

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ class TelemetryCollector
1212
{
1313
use MakesHttpRequests;
1414

15-
protected const MAX_TOOLS_PER_FLUSH = 20;
16-
17-
public array $toolCounts = [];
15+
public array $toolData = [];
1816

1917
protected bool $enabled;
2018

@@ -24,8 +22,11 @@ class TelemetryCollector
2422

2523
protected string $laravelVersion;
2624

25+
protected float $sessionStartTime;
26+
2727
public function __construct()
2828
{
29+
$this->sessionStartTime = microtime(true);
2930
$this->enabled = config('boost.telemetry.enabled', false);
3031
if ($this->enabled) {
3132
$this->url = config('boost.telemetry.url', 'https://boost.laravel.com/api/telemetry');
@@ -46,23 +47,28 @@ public function __destruct()
4647
$this->flush();
4748
}
4849

49-
public function record(string $toolName): void
50+
public function record(string $toolName, int $wordCount): void
5051
{
5152
if (! $this->enabled) {
5253
return;
5354
}
5455

55-
$totalCount = array_sum($this->toolCounts);
56-
if ($totalCount >= self::MAX_TOOLS_PER_FLUSH) {
57-
$this->flush();
56+
if (! isset($this->toolData[$toolName])) {
57+
$this->toolData[$toolName] = [];
5858
}
5959

60-
$this->toolCounts[$toolName] = ($this->toolCounts[$toolName] ?? 0) + 1;
60+
$tokens = $this->calculateTokens($wordCount);
61+
$this->toolData[$toolName][] = ['tokens' => $tokens];
62+
}
63+
64+
protected function calculateTokens(int $wordCount): int
65+
{
66+
return (int) round($wordCount * 1.3);
6167
}
6268

6369
public function flush(): void
6470
{
65-
if ($this->toolCounts === [] || ! $this->enabled) {
71+
if ($this->toolData === [] || ! $this->enabled) {
6672
return;
6773
}
6874

@@ -73,22 +79,40 @@ public function flush(): void
7379
} catch (Throwable) {
7480
//
7581
} finally {
76-
$this->toolCounts = [];
82+
$this->toolData = [];
83+
$this->sessionStartTime = microtime(true);
7784
}
7885
}
7986

8087
protected function buildPayload(): string
8188
{
8289
$version = InstalledVersions::getVersion('laravel/boost');
90+
$sessionEndTime = microtime(true);
8391

8492
return base64_encode(json_encode([
8593
'session_id' => $this->sessionId,
8694
'boost_version' => $version,
8795
'php_version' => PHP_VERSION,
8896
'os' => PHP_OS_FAMILY,
8997
'laravel_version' => $this->laravelVersion,
90-
'tools' => $this->toolCounts,
98+
'session_start' => date('c', (int) $this->sessionStartTime),
99+
'session_end' => date('c', (int) $sessionEndTime),
100+
'tools' => $this->formatToolsData(),
91101
'timestamp' => date('c'),
92102
]));
93103
}
104+
105+
protected function formatToolsData(): array
106+
{
107+
$formatted = [];
108+
109+
foreach ($this->toolData as $toolName => $invocations) {
110+
$formatted[$toolName] = [];
111+
foreach ($invocations as $index => $invocation) {
112+
$formatted[$toolName][(string) ($index + 1)] = $invocation;
113+
}
114+
}
115+
116+
return $formatted;
117+
}
94118
}

tests/Unit/Telemetry/TelemetryCollectorTest.php

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,34 @@
88

99
beforeEach(function (): void {
1010
$this->collector = app(TelemetryCollector::class);
11-
$this->collector->toolCounts = [];
11+
$this->collector->toolData = [];
1212
});
1313

1414
it('records tool invocations', function (): void {
1515
config(['boost.telemetry.enabled' => true]);
1616

17-
$this->collector->record(DatabaseQuery::class);
18-
$this->collector->record(DatabaseQuery::class);
19-
$this->collector->record(Tinker::class);
20-
21-
expect($this->collector->toolCounts)->toBe([
22-
DatabaseQuery::class => 2,
23-
Tinker::class => 1,
17+
$this->collector->record(DatabaseQuery::class, 100);
18+
$this->collector->record(DatabaseQuery::class, 200);
19+
$this->collector->record(Tinker::class, 150);
20+
21+
expect($this->collector->toolData)->toBe([
22+
DatabaseQuery::class => [
23+
['tokens' => 130], // 100 * 1.3
24+
['tokens' => 260], // 200 * 1.3
25+
],
26+
Tinker::class => [
27+
['tokens' => 195], // 150 * 1.3
28+
],
2429
]);
2530
});
2631

2732
it('does not record when disabled via config', function (): void {
2833
config(['boost.telemetry.enabled' => false]);
2934

3035
$collector = new TelemetryCollector;
31-
$collector->record(DatabaseQuery::class);
36+
$collector->record(DatabaseQuery::class, 100);
3237

33-
expect($collector->toolCounts)->toBe([]);
34-
});
35-
36-
it('auto-flushes when reaching MAX_TOOLS_PER_FLUSH', function (): void {
37-
config(['boost.telemetry.enabled' => true]);
38-
39-
Http::fake([
40-
'*' => Http::response(['status' => 'ok'], 200),
41-
]);
42-
43-
for ($i = 0; $i < 20; $i++) {
44-
$this->collector->record(Tinker::class);
45-
}
46-
47-
expect($this->collector->toolCounts)->toHaveCount(1)
48-
->and($this->collector->toolCounts[Tinker::class])->toBe(20);
49-
50-
$this->collector->record(Tinker::class);
51-
52-
expect(Http::recorded())->toHaveCount(1)
53-
->and($this->collector->toolCounts)->toHaveCount(1)
54-
->and($this->collector->toolCounts[Tinker::class])->toBe(1);
55-
});
56-
57-
it('does not auto-flush below MAX_TOOLS_PER_FLUSH', function (): void {
58-
config(['boost.telemetry.enabled' => true]);
59-
60-
Http::fake([
61-
'*' => Http::response(['status' => 'ok'], 200),
62-
]);
63-
64-
for ($i = 0; $i < 19; $i++) {
65-
$this->collector->record(Tinker::class);
66-
}
67-
68-
expect(Http::recorded())->toHaveCount(0)
69-
->and($this->collector->toolCounts)->toHaveCount(1)
70-
->and($this->collector->toolCounts[Tinker::class])->toBe(19);
38+
expect($collector->toolData)->toBe([]);
7139
});
7240

7341
it('flush sends data and clears counts', function (): void {
@@ -77,7 +45,7 @@
7745
'*' => Http::response(['status' => 'ok'], 200),
7846
]);
7947

80-
$this->collector->record(Tinker::class);
48+
$this->collector->record(Tinker::class, 150);
8149
$this->collector->flush();
8250

8351
expect(Http::recorded())->toHaveCount(1);
@@ -86,11 +54,11 @@
8654
$payload = json_decode(base64_decode((string) $request['data'], true), true);
8755

8856
expect($request->url())->toBe(config('boost.telemetry.url'))
89-
->and($payload['tools'][Tinker::class])->toBe(1)
90-
->and($this->collector->toolCounts)->toBe([]);
57+
->and($payload['tools'][Tinker::class]['1'])->toBe(['tokens' => 195]) // 150 * 1.3
58+
->and($this->collector->toolData)->toBe([]);
9159
});
9260

93-
it('flush does nothing when toolCounts is empty', function (): void {
61+
it('flush does nothing when toolData is empty', function (): void {
9462
config(['boost.telemetry.enabled' => true]);
9563

9664
Http::fake([
@@ -110,7 +78,7 @@
11078
]);
11179

11280
$collector = new TelemetryCollector;
113-
$collector->toolCounts = ['SomeTool' => 1];
81+
$collector->toolData = ['SomeTool' => [['tokens' => 100]]];
11482
$collector->flush();
11583

11684
expect(Http::recorded())->toHaveCount(0);
@@ -123,10 +91,10 @@
12391
'*' => Http::response(null, 500),
12492
]);
12593

126-
$this->collector->record(Tinker::class);
94+
$this->collector->record(Tinker::class, 100);
12795
$this->collector->flush();
12896

129-
expect($this->collector->toolCounts)->toBe([]);
97+
expect($this->collector->toolData)->toBe([]);
13098
});
13199

132100
it('flush fails silently on connection timeout', function (): void {
@@ -136,10 +104,10 @@
136104
throw new \Exception('Connection timeout');
137105
});
138106

139-
$this->collector->record(Tinker::class);
107+
$this->collector->record(Tinker::class, 100);
140108
$this->collector->flush();
141109

142-
expect($this->collector->toolCounts)->toBe([]);
110+
expect($this->collector->toolData)->toBe([]);
143111
});
144112

145113
it('includes buildPayload as the correct structure', function (): void {
@@ -149,18 +117,31 @@
149117
'*' => Http::response(['status' => 'ok'], 200),
150118
]);
151119

152-
$this->collector->record(Tinker::class);
120+
$this->collector->record(Tinker::class, 100);
153121
$this->collector->flush();
154122

155123
expect(Http::recorded())->toHaveCount(1);
156124

157125
$request = Http::recorded()[0][0];
158126
$payload = json_decode(base64_decode((string) $request['data'], true), true);
159127

160-
expect($payload)->toHaveKeys(['session_id', 'boost_version', 'php_version', 'os', 'laravel_version', 'tools', 'timestamp'])
128+
expect($payload)->toHaveKeys([
129+
'session_id',
130+
'boost_version',
131+
'php_version',
132+
'os',
133+
'laravel_version',
134+
'session_start',
135+
'session_end',
136+
'tools',
137+
'timestamp',
138+
])
161139
->and($payload['php_version'])->toBe(PHP_VERSION)
162140
->and($payload['os'])->toBe(PHP_OS_FAMILY)
163-
->and($payload['tools'])->toBeArray();
141+
->and($payload['tools'])->toBeArray()
142+
->and($payload['tools'][Tinker::class]['1']['tokens'])->toBe(130) // 100 * 1.3
143+
->and(strtotime((string) $payload['session_start']))->not->toBeFalse()
144+
->and(strtotime((string) $payload['session_end']))->not->toBeFalse();
164145
});
165146

166147
it('sends session_id as a consistent hash of base_path', function (): void {
@@ -172,7 +153,7 @@
172153

173154
$expectedSessionId = hash('sha256', base_path());
174155

175-
$this->collector->record(Tinker::class);
156+
$this->collector->record(Tinker::class, 100);
176157
$this->collector->flush();
177158

178159
expect(Http::recorded())->toHaveCount(1);
@@ -183,6 +164,28 @@
183164
expect($payload['session_id'])->toBe($expectedSessionId);
184165
});
185166

167+
it('records tool response sizes and resets after flush', function (): void {
168+
config(['boost.telemetry.enabled' => true]);
169+
170+
Http::fake([
171+
'*' => Http::response(['status' => 'ok'], 200),
172+
]);
173+
174+
$this->collector->record(Tinker::class, 128);
175+
$this->collector->record(Tinker::class, 256);
176+
177+
$this->collector->flush();
178+
179+
expect(Http::recorded())->toHaveCount(1);
180+
181+
$request = Http::recorded()[0][0];
182+
$payload = json_decode(base64_decode((string) $request['data'], true), true);
183+
184+
expect($payload['tools'][Tinker::class]['1']['tokens'])->toBe(166) // 128 * 1.3
185+
->and($payload['tools'][Tinker::class]['2']['tokens'])->toBe(333) // 256 * 1.3
186+
->and($this->collector->toolData)->toBe([]);
187+
});
188+
186189
it('uses boost_version as InstalledVersions', function (): void {
187190
config(['boost.telemetry.enabled' => true]);
188191

@@ -192,7 +195,7 @@
192195

193196
$expectedVersion = InstalledVersions::getVersion('laravel/boost');
194197

195-
$this->collector->record(Tinker::class);
198+
$this->collector->record(Tinker::class, 100);
196199
$this->collector->flush();
197200

198201
expect(Http::recorded())->toHaveCount(1);

0 commit comments

Comments
 (0)