Skip to content
58 changes: 56 additions & 2 deletions src/Concerns/CapturesState.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@
use function array_unshift;
use function debug_backtrace;
use function env;
use function in_array;
use function memory_reset_peak_usage;
use function preg_match;
use function random_int;
use function str_contains;

/**
* @internal
Expand All @@ -59,6 +61,8 @@ trait CapturesState

private bool $paused = false;

private bool $captureDefaultVendorCommands = false;

/**
* @var WeakMap<Event, float>
*/
Expand Down Expand Up @@ -114,8 +118,14 @@ public function configureRequestSampling(): void
/**
* @internal
*/
public function configureCommandSampling(): void
public function configureCommandSampling(string $command): void
{
if (! $this->captureDefaultVendorCommands && in_array($command, $this->defaultVendorCommands(), true)) {
$this->dontSample();

return;
}

$this->sample(match (Compatibility::getSamplingFromContext(null)) {
true => 1.0,
false => 0.0,
Expand All @@ -128,7 +138,51 @@ public function configureCommandSampling(): void
*/
public function configureScheduledTaskSampling(Event $event): void
{
$this->sample(rate: $this->scheduledTasksSampleRates[$event] ?? $this->config['sampling']['scheduled_tasks']);
$rate = $this->config['sampling']['scheduled_tasks'];

if (! $this->captureDefaultVendorCommands) {
foreach ($this->defaultVendorCommands() as $command) {
if (str_contains($event->command ?? '', $command)) {
$rate = 0.0;

break;
}
}
}

$this->sample(rate: $this->scheduledTasksSampleRates[$event] ?? $rate);
}

/**
* @api
*/
public function captureDefaultVendorCommands(bool $capture = true): void
{
$this->captureDefaultVendorCommands = $capture;
}

/**
* @api
*
* @return list<string>
*/
public static function defaultVendorCommands(): array
{
return [
'auth:clear-resets',
'config:cache',
'horizon:snapshot',
'horizon:status',
'horizon:supervisor',
'inertia:start-ssr',
'invoke-serialized-closure',
'model:prune',
'nightwatch:agent',
'nightwatch:status',
'queue:monitor',
'reverb:start',
'schedule:list',
];
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Facades/Nightwatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
* @method static void rejectCacheKeys(array $keys)
* @method static void captureDefaultVendorCacheKeys(bool $capture = true)
* @method static array defaultVendorCacheKeys()
* @method static void captureDefaultVendorCommands(bool $capture = true)
* @method static array defaultVendorCommands()
* @method static void rejectMail(callable $callback)
* @method static void rejectNotifications(callable $callback)
* @method static void rejectOutgoingRequests(callable $callback)
Expand Down
4 changes: 2 additions & 2 deletions src/Hooks/CommandStartingListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function __invoke(CommandStarting $event): void
match ($event->command) {
'queue:work', 'queue:listen', 'horizon:work', 'vapor:work' => $this->registerJobHooks($event),
'schedule:run', 'schedule:work' => $this->registerScheduledTaskHooks(),
'schedule:finish' => null,
'help', 'inspire', 'schedule:finish' => null,
default => $this->registerCommandHooks($event),
};
} catch (Throwable $e) {
Expand Down Expand Up @@ -116,7 +116,7 @@ private function registerCommandHooks(CommandStarting $event): void
return;
}

$this->nightwatch->configureCommandSampling();
$this->nightwatch->configureCommandSampling($event->command);

$this->nightwatch->prepareForCommand($event->command);

Expand Down
23 changes: 23 additions & 0 deletions tests/Feature/Sensors/CommandSensorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Illuminate\Support\Facades\DB;
use Laravel\Nightwatch\Compatibility;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\NullOutput;
use Tests\TestCase;

use function array_shift;
Expand Down Expand Up @@ -285,6 +286,28 @@ public function test_it_ignores_schedule_finish_command(): void
$this->assertSame(0, $status);
$ingest->assertWrittenTimes(0);
}

public function test_it_ignores_inspire_command(): void
{
$ingest = $this->fakeIngest();

$status = Artisan::handle($input = new StringInput('inspire'), new NullOutput);
Artisan::terminate($input, $status);

$this->assertSame(0, $status);
$ingest->assertWrittenTimes(0);
}

public function test_it_ignores_help_command(): void
{
$ingest = $this->fakeIngest();

$status = Artisan::handle($input = new StringInput('help'), new NullOutput);
Artisan::terminate($input, $status);

$this->assertSame(0, $status);
$ingest->assertWrittenTimes(0);
}
}

class ParentCommand extends Command
Expand Down
93 changes: 91 additions & 2 deletions tests/Unit/CliSamplingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
use Laravel\Nightwatch\Compatibility;
use Laravel\Nightwatch\Console\Sample;
use Laravel\Nightwatch\Facades\Nightwatch;
use PHPUnit\Framework\Attributes\DataProvider;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\NullOutput;
use Tests\TestCase;

use function dirname;
use function event;
use function fake;

class CliSamplingTest extends TestCase
{
Expand All @@ -27,6 +30,8 @@ protected function setUp(): void
$this->forceCommandExecutionState();

parent::setUp();

$this->app->setBasePath(dirname($this->app->basePath()));
}

public function test_it_samples_job_attempts(): void
Expand Down Expand Up @@ -129,9 +134,14 @@ public function test_it_pulls_sample_from_context_when_command_starting(): void
public function test_it_resets_sampling_after_each_task(): void
{
event(new CommandStarting('schedule:run', new StringInput(''), new NullOutput));
Artisan::command($command = fake()->name(), fn () => 0);

Nightwatch::dontSample();
event(new ScheduledTaskStarting($this->app[Schedule::class]->call('php artisan inspire')));
event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command)));
$this->assertTrue(Nightwatch::sampling());

Artisan::command($command = fake()->name(), fn () => 0);
event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command)));

$this->assertTrue(Nightwatch::sampling());
}
Expand All @@ -151,7 +161,7 @@ public function test_it_can_use_global_config_to_sample_scheduled_tasks(): void
$ingest->forgetWrites();
}

$this->assertEqualsWithDelta(50, $writes, 10);
$this->assertEqualsWithDelta(50, $writes, 20);
$this->assertCount(0, $this->core->ingest->buffer);
}

Expand All @@ -175,4 +185,83 @@ public function test_it_applies_individual_sample_rates_to_scheduled_tasks(): vo
$this->assertSame(100, $writes);
$this->assertCount(0, $this->core->ingest->buffer);
}

#[DataProvider('vendorCommands')]
public function test_it_samples_vendor_commands_separately(string $command): void
{
$ingest = $this->fakeIngest();
Artisan::command($command, fn () => 0);

$status = Artisan::handle($input = new StringInput($command), new NullOutput);
Artisan::terminate($input, $status);

$ingest->assertWrittenTimes(0);
}

#[DataProvider('vendorCommands')]
public function test_it_samples_vendor_commands_when_enabled(string $command): void
{
$ingest = $this->fakeIngest();
Artisan::command($command, fn () => 0);

Nightwatch::captureDefaultVendorCommands();

$status = Artisan::handle($input = new StringInput($command), new NullOutput);
Artisan::terminate($input, $status);

$ingest->assertWrittenTimes(1);
}

#[DataProvider('vendorCommands')]
public function test_it_samples_vendor_scheduled_tasks_separately(string $command): void
{
event(new CommandStarting('schedule:run', new StringInput(''), new NullOutput));

event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command)->everyMinute()));

$this->assertFalse(Nightwatch::sampling());
}

#[DataProvider('vendorCommands')]
public function test_it_samples_vendor_scheduled_tasks_when_enabled(string $command): void
{
Nightwatch::captureDefaultVendorCommands();

event(new CommandStarting('schedule:run', new StringInput(''), new NullOutput));

event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command)->everyMinute()));

$this->assertTrue(Nightwatch::sampling());
}

#[DataProvider('vendorCommands')]
public function test_it_samples_vendor_scheduled_tasks_when_explicitly_sampled(string $command): void
{
event(new CommandStarting('schedule:run', new StringInput(''), new NullOutput));

$samples = 0;
for ($i = 0; $i < 100; $i++) {
event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command)->everyMinute()->tap(Sample::rate(0.5))));
$samples += (int) Nightwatch::sampling();
}

$this->assertEqualsWithDelta(50, $samples, 20);
}

public static function vendorCommands(): iterable
{
yield ['auth:clear-resets'];
yield ['config:cache'];
yield ['horizon:snapshot'];
yield ['horizon:status'];
yield ['horizon:supervisor'];
yield ['inertia:start-ssr'];
yield ['invoke-serialized-closure'];
yield ['model:prune'];
yield ['nightwatch:agent'];
yield ['nightwatch:status'];
yield ['queue:monitor'];
yield ['reverb:start'];
yield ['schedule:list'];
}
}