feat(stats): add --no-discord to the remaining report commands (#607)
## What Follow-up to the `--no-discord` flag added to `stats:experiment-funnel`. Extends the same flag to the **other stats report commands that post to Discord**, so any of them can be run on demand (e.g. inside the prod container) and just printed to the console without posting: ```bash php artisan stats:subscription-funnel --no-discord php artisan stats:ai-cohort-report --no-discord php artisan stats:stuck-cohort-report --no-discord php artisan stats:daily-report --no-discord ``` ## How - The two **funnel** commands (`subscription-funnel`) already printed their table to the console, so `--no-discord` just short-circuits the `->send()`. - The **cohort/daily** commands only ever built a Discord embed. A small `App\Console\Commands\Concerns\RendersReportToConsole` trait renders an embed (title / description / fields) as plain text, so `--no-discord` prints a readable report to the console instead of posting. - Default behaviour is unchanged: without the flag (and on the scheduled weekly/daily runs) they post to Discord exactly as before. ## Tests A `--no-discord` test added to each command (`Http::assertNothingSent()` + the command succeeds). Pint + Larastan + all four command suites green. > Note: the commands query the default DB connection, so for **production** numbers run them inside the prod container (Coolify terminal).
This commit is contained in:
parent
1db2871398
commit
300756e553
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands\Concerns;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/**
|
||||
* Render Discord report embeds as plain console output, for the --no-discord
|
||||
* flag on the stats:* report commands.
|
||||
*
|
||||
* @mixin Command
|
||||
*/
|
||||
trait RendersReportToConsole
|
||||
{
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $embeds
|
||||
*/
|
||||
protected function printEmbeds(array $embeds): void
|
||||
{
|
||||
foreach ($embeds as $embed) {
|
||||
if (! empty($embed['title'])) {
|
||||
$this->newLine();
|
||||
$this->line('<options=bold>'.$this->plainText($embed['title']).'</>');
|
||||
}
|
||||
|
||||
if (! empty($embed['description'])) {
|
||||
$this->line($this->plainText($embed['description']));
|
||||
}
|
||||
|
||||
foreach ($embed['fields'] ?? [] as $field) {
|
||||
$this->newLine();
|
||||
$this->line('<options=bold>'.$this->plainText($field['name']).'</>');
|
||||
$this->line($this->plainText($field['value']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip Discord markdown (code fences, bold) for readable console output.
|
||||
*/
|
||||
private function plainText(string $text): string
|
||||
{
|
||||
return trim(str_replace(['```', '**'], '', $text));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Console\Commands\Concerns\RendersReportToConsole;
|
||||
use App\Services\Ai\AiCohortReportCollector;
|
||||
use App\Services\Discord\DiscordWebhook;
|
||||
use Carbon\CarbonImmutable;
|
||||
|
|
@ -9,7 +10,9 @@ use Illuminate\Console\Command;
|
|||
|
||||
class SendAiCohortReportCommand extends Command
|
||||
{
|
||||
protected $signature = 'stats:ai-cohort-report {--weeks= : Number of weekly cohorts to include}';
|
||||
use RendersReportToConsole;
|
||||
|
||||
protected $signature = 'stats:ai-cohort-report {--weeks= : Number of weekly cohorts to include} {--no-discord : Print the report to the console only, without posting to Discord}';
|
||||
|
||||
protected $description = 'Post the weekly AI-suggestions cohort retention/conversion report to Discord';
|
||||
|
||||
|
|
@ -23,11 +26,19 @@ class SendAiCohortReportCommand extends Command
|
|||
$weeks = $this->option('weeks') !== null ? (int) $this->option('weeks') : null;
|
||||
|
||||
$report = $this->collector->collect($weeks);
|
||||
$embed = $this->buildEmbed($report);
|
||||
|
||||
if ($this->option('no-discord')) {
|
||||
$this->printEmbeds([$embed]);
|
||||
$this->info('Skipped Discord (--no-discord).');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$webhookUrl = config('services.discord.ai_cohort_webhook_url')
|
||||
?: config('services.discord.webhook_url');
|
||||
|
||||
(new DiscordWebhook($webhookUrl))->send('', [$this->buildEmbed($report)]);
|
||||
(new DiscordWebhook($webhookUrl))->send('', [$embed]);
|
||||
|
||||
$this->info('AI cohort report sent to Discord.');
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Console\Commands\Concerns\RendersReportToConsole;
|
||||
use App\Models\User;
|
||||
use App\Services\Discord\DiscordWebhook;
|
||||
use App\Services\Stripe\SubscriptionStatsCollector;
|
||||
|
|
@ -11,7 +12,9 @@ use Stripe\Exception\ApiErrorException;
|
|||
|
||||
class SendDailyStatsReportCommand extends Command
|
||||
{
|
||||
protected $signature = 'stats:daily-report';
|
||||
use RendersReportToConsole;
|
||||
|
||||
protected $signature = 'stats:daily-report {--no-discord : Print the report to the console only, without posting to Discord}';
|
||||
|
||||
protected $description = 'Post yesterday\'s user and Stripe subscription stats to the Discord admin channel';
|
||||
|
||||
|
|
@ -45,9 +48,16 @@ class SendDailyStatsReportCommand extends Command
|
|||
->where('created_at', '<', $todayStart->copy()->utc())
|
||||
->count();
|
||||
|
||||
$this->discord->send('', [
|
||||
$this->buildEmbed($stats, $newUsers, $totalUsers, $yesterdayStart),
|
||||
]);
|
||||
$embed = $this->buildEmbed($stats, $newUsers, $totalUsers, $yesterdayStart);
|
||||
|
||||
if ($this->option('no-discord')) {
|
||||
$this->printEmbeds([$embed]);
|
||||
$this->info('Skipped Discord (--no-discord).');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->discord->send('', [$embed]);
|
||||
|
||||
$this->info('Daily stats report sent to Discord.');
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Console\Commands\Concerns\RendersReportToConsole;
|
||||
use App\Models\StuckCohortSnapshot;
|
||||
use App\Services\Discord\DiscordWebhook;
|
||||
use App\Services\Stats\StuckCohortReportCollector;
|
||||
|
|
@ -9,7 +10,9 @@ use Illuminate\Console\Command;
|
|||
|
||||
class SendStuckCohortReportCommand extends Command
|
||||
{
|
||||
protected $signature = 'stats:stuck-cohort-report';
|
||||
use RendersReportToConsole;
|
||||
|
||||
protected $signature = 'stats:stuck-cohort-report {--no-discord : Print the report to the console only, without posting to Discord}';
|
||||
|
||||
protected $description = 'Post the weekly paywall stuck-cohort report (banked users without a valid subscription) to Discord';
|
||||
|
||||
|
|
@ -21,11 +24,19 @@ class SendStuckCohortReportCommand extends Command
|
|||
public function handle(): int
|
||||
{
|
||||
$report = $this->collector->collect();
|
||||
$embed = $this->buildEmbed($report);
|
||||
|
||||
if ($this->option('no-discord')) {
|
||||
$this->printEmbeds([$embed]);
|
||||
$this->info('Skipped Discord (--no-discord).');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$webhookUrl = config('services.discord.ai_cohort_webhook_url')
|
||||
?: config('services.discord.webhook_url');
|
||||
|
||||
(new DiscordWebhook($webhookUrl))->send('', [$this->buildEmbed($report)]);
|
||||
(new DiscordWebhook($webhookUrl))->send('', [$embed]);
|
||||
|
||||
$this->info('Stuck cohort report sent to Discord.');
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use Illuminate\Console\Command;
|
|||
|
||||
class SendSubscriptionFunnelReportCommand extends Command
|
||||
{
|
||||
protected $signature = 'stats:subscription-funnel {--weeks= : Number of weekly cohorts to include}';
|
||||
protected $signature = 'stats:subscription-funnel {--weeks= : Number of weekly cohorts to include} {--no-discord : Print the report to the console only, without posting to Discord}';
|
||||
|
||||
protected $description = 'Post the weekly registration -> subscription -> paid funnel to Discord';
|
||||
|
||||
|
|
@ -27,6 +27,12 @@ class SendSubscriptionFunnelReportCommand extends Command
|
|||
$this->line($line);
|
||||
}
|
||||
|
||||
if ($this->option('no-discord')) {
|
||||
$this->info('Skipped Discord (--no-discord).');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$webhookUrl = config('services.discord.ai_cohort_webhook_url')
|
||||
?: config('services.discord.webhook_url');
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,17 @@ it('posts the cohort report embed to the configured discord webhook', function (
|
|||
});
|
||||
});
|
||||
|
||||
it('prints to the console without posting when --no-discord is set', function () {
|
||||
config(['services.discord.ai_cohort_webhook_url' => 'https://discord.test/hook']);
|
||||
Http::fake(['discord.test/*' => Http::response('', 204)]);
|
||||
|
||||
cohortUser(referenceNow()->subWeeks(6), ['transactions' => 3, 'lastActiveAt' => referenceNow()->subWeeks(3)]);
|
||||
|
||||
artisan('stats:ai-cohort-report', ['--no-discord' => true])->assertSuccessful();
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('falls back to the default discord webhook when no dedicated one is set', function () {
|
||||
config([
|
||||
'services.discord.ai_cohort_webhook_url' => null,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,20 @@ test('posts yesterday user counts and stripe stats to discord', function () {
|
|||
});
|
||||
});
|
||||
|
||||
test('prints to the console without posting when --no-discord is set', function () {
|
||||
Http::fake();
|
||||
bindMockStripeClientForStats([
|
||||
'active' => [makeStripeSubscription('eur', 1000, 'month')],
|
||||
'trialing' => [],
|
||||
]);
|
||||
|
||||
User::factory()->create(['created_at' => Carbon::now('Europe/Madrid')->subDay()->setTime(12, 0)->utc()]);
|
||||
|
||||
$this->artisan('stats:daily-report', ['--no-discord' => true])->assertSuccessful();
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
test('total reflects users at end of yesterday and excludes users created today', function () {
|
||||
Http::fake();
|
||||
bindMockStripeClientForStats(['active' => [], 'trialing' => []]);
|
||||
|
|
|
|||
|
|
@ -132,6 +132,15 @@ it('upserts a single snapshot per day across runs', function () {
|
|||
expect(StuckCohortSnapshot::query()->whereDate('date', today())->count())->toBe(1);
|
||||
});
|
||||
|
||||
it('prints to the console without posting when --no-discord is set', function () {
|
||||
stuckUser();
|
||||
subscribedUser('active');
|
||||
|
||||
artisan('stats:stuck-cohort-report', ['--no-discord' => true])->assertSuccessful();
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('posts the stuck cohort embed to the configured discord webhook', function () {
|
||||
stuckUser();
|
||||
subscribedUser('active');
|
||||
|
|
|
|||
|
|
@ -142,3 +142,14 @@ it('posts the funnel embed to the configured discord webhook', function () {
|
|||
&& str_contains($request['embeds'][0]['title'], 'Subscription Funnel');
|
||||
});
|
||||
});
|
||||
|
||||
it('prints to the console without posting when --no-discord is set', function () {
|
||||
config(['services.discord.ai_cohort_webhook_url' => 'https://discord.test/hook']);
|
||||
Http::fake(['discord.test/*' => Http::response('', 204)]);
|
||||
|
||||
funnelUser(funnelNow()->subWeeks(10), ['status' => 'active', 'at' => funnelNow()->subWeeks(10)->addDays(2)]);
|
||||
|
||||
artisan('stats:subscription-funnel', ['--no-discord' => true])->assertSuccessful();
|
||||
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue