Roadmap 6.1/6.16: Block 1 bis 3 mit Ist, zeitanteiligem Soll, Schwelle und Status, Trichter seit 17.09., Transparenzseiten aus Umami. Neue Befehle growth:report und umami:report. Zugang über UMAMI_API_* (View-only-Benutzer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.1 KiB
PHP
103 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Services\Analytics\UmamiClient;
|
|
use App\Services\Analytics\UmamiUnavailable;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
/**
|
|
* Nur lesende Abfrage der Umami-Webanalyse (Roadmap 6.16): Kennzahlen je
|
|
* Website, dazu eine Rangliste (Seiten, Herkunft, Kanäle …), optional auf
|
|
* Pfade gefiltert. Zugangsdaten aus `UMAMI_API_*`, werden nie ausgegeben.
|
|
*/
|
|
class ReportUmami extends Command
|
|
{
|
|
protected $signature = 'umami:report
|
|
{--from= : Startdatum (Standard: vor 30 Tagen)}
|
|
{--to= : Enddatum (Standard: jetzt)}
|
|
{--site=* : pressekonto, presseecho, businessportal24 (Standard: alle)}
|
|
{--path= : nur Pfade, die diesen Text enthalten, z. B. gute-pressemitteilung}
|
|
{--metric=channel : Rangliste nach path, referrer, channel, country, device, browser, event}
|
|
{--limit=10 : Zeilen der Rangliste}
|
|
{--json : Ausgabe als JSON statt als Tabelle}';
|
|
|
|
protected $description = 'Umami-Kennzahlen je Website (nur lesend).';
|
|
|
|
private const array METRICS = ['path', 'referrer', 'channel', 'country', 'device', 'browser', 'os', 'event', 'title', 'entry', 'exit'];
|
|
|
|
public function handle(UmamiClient $umami): int
|
|
{
|
|
if (! $umami->isConfigured()) {
|
|
$this->error('Umami-API nicht konfiguriert: UMAMI_API_URL, UMAMI_API_USERNAME und UMAMI_API_PASSWORD setzen.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$metric = (string) $this->option('metric');
|
|
|
|
if (! in_array($metric, self::METRICS, true)) {
|
|
$this->error('Unbekannte Rangliste. Möglich: '.implode(', ', self::METRICS).'.');
|
|
|
|
return self::INVALID;
|
|
}
|
|
|
|
$to = $this->option('to') !== null ? Carbon::parse((string) $this->option('to'))->endOfDay() : Carbon::now();
|
|
$from = $this->option('from') !== null ? Carbon::parse((string) $this->option('from'))->startOfDay() : $to->copy()->subDays(30);
|
|
$sites = (array) $this->option('site') ?: UmamiClient::BRANDS;
|
|
$filters = filled($this->option('path')) ? ['path' => 'c.'.$this->option('path')] : [];
|
|
$limit = max(1, min(500, (int) $this->option('limit')));
|
|
|
|
$result = [];
|
|
|
|
try {
|
|
foreach ($sites as $site) {
|
|
$websiteId = $umami->websiteId((string) $site);
|
|
|
|
if ($websiteId === null) {
|
|
$this->warn("Keine Umami-Website für „{$site}“ in config/domains.php.");
|
|
|
|
continue;
|
|
}
|
|
|
|
$result[$site] = [
|
|
'stats' => $umami->stats($websiteId, $from, $to, $filters),
|
|
$metric => $umami->metrics($websiteId, $metric, $from, $to, $filters, $limit),
|
|
];
|
|
}
|
|
} catch (UmamiUnavailable $e) {
|
|
$this->error($e->getMessage());
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if ($this->option('json')) {
|
|
$this->line((string) json_encode([
|
|
'from' => $from->toIso8601String(),
|
|
'to' => $to->toIso8601String(),
|
|
'filters' => $filters,
|
|
'sites' => $result,
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$this->info(sprintf('Umami — %s bis %s%s', $from->format('d.m.Y'), $to->format('d.m.Y'), $filters === [] ? '' : ', Pfad enthält „'.$this->option('path').'“'));
|
|
|
|
foreach ($result as $site => $data) {
|
|
$this->newLine();
|
|
$this->line("<comment>{$site}</comment>");
|
|
$this->table(
|
|
['Aufrufe', 'Besucher', 'Besuche', 'Absprünge'],
|
|
[[$data['stats']['pageviews'], $data['stats']['visitors'], $data['stats']['visits'], $data['stats']['bounces']]],
|
|
);
|
|
$this->table(
|
|
[ucfirst($metric), 'Anzahl'],
|
|
array_map(static fn (array $row): array => [$row['x'], $row['y']], $data[$metric]),
|
|
);
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|