From 8e604e291a8aee9a8020e3e7152ad60a7e6a08b9 Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 20:38:15 +0200 Subject: [PATCH 01/11] Enhancement #5377 adding telemetry --- .../RequestHandlers/TelemetryPreviewPage.php | 58 ++++++ .../TelemetrySettingsAction.php | 47 +++++ .../RequestHandlers/TelemetrySettingsPage.php | 45 +++++ .../RequestHandlers/TelemetrySubmitAction.php | 78 +++++++ app/Http/Routes/WebRoutes.php | 8 + app/Services/TelemetryDataService.php | 191 ++++++++++++++++++ resources/views/admin/control-panel.phtml | 15 ++ resources/views/admin/telemetry-preview.phtml | 46 +++++ .../views/admin/telemetry-settings.phtml | 61 ++++++ resources/views/icons/telemetry.phtml | 1 + 10 files changed, 550 insertions(+) create mode 100644 app/Http/RequestHandlers/TelemetryPreviewPage.php create mode 100644 app/Http/RequestHandlers/TelemetrySettingsAction.php create mode 100644 app/Http/RequestHandlers/TelemetrySettingsPage.php create mode 100644 app/Http/RequestHandlers/TelemetrySubmitAction.php create mode 100644 app/Services/TelemetryDataService.php create mode 100644 resources/views/admin/telemetry-preview.phtml create mode 100644 resources/views/admin/telemetry-settings.phtml create mode 100644 resources/views/icons/telemetry.phtml diff --git a/app/Http/RequestHandlers/TelemetryPreviewPage.php b/app/Http/RequestHandlers/TelemetryPreviewPage.php new file mode 100644 index 00000000000..03dc8826ccf --- /dev/null +++ b/app/Http/RequestHandlers/TelemetryPreviewPage.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Http\RequestHandlers; + +use Fisharebest\Webtrees\Http\ViewResponseTrait; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Services\TelemetryDataService; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +use function json_encode; + +use const JSON_PRETTY_PRINT; +use const JSON_UNESCAPED_SLASHES; +use const JSON_UNESCAPED_UNICODE; + +final class TelemetryPreviewPage implements RequestHandlerInterface +{ + use ViewResponseTrait; + + public function __construct( + private readonly TelemetryDataService $telemetry_data_service, + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->layout = 'layouts/administration'; + + $payload = $this->telemetry_data_service->assemblePayload(); + $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + $title = I18N::translate('Telemetry data preview'); + + return $this->viewResponse('admin/telemetry-preview', [ + 'title' => $title, + 'payload' => $json, + ]); + } +} diff --git a/app/Http/RequestHandlers/TelemetrySettingsAction.php b/app/Http/RequestHandlers/TelemetrySettingsAction.php new file mode 100644 index 00000000000..73ad44cb0f5 --- /dev/null +++ b/app/Http/RequestHandlers/TelemetrySettingsAction.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Http\RequestHandlers; + +use Fisharebest\Webtrees\FlashMessages; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Site; +use Fisharebest\Webtrees\Validator; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +use function redirect; +use function route; + +final class TelemetrySettingsAction implements RequestHandlerInterface +{ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $endpointUrl = Validator::parsedBody($request)->string('TELEMETRY_ENDPOINT_URL'); + $publishableKey = Validator::parsedBody($request)->string('TELEMETRY_PUBLISHABLE_KEY'); + + Site::setPreference('TELEMETRY_ENDPOINT_URL', $endpointUrl); + Site::setPreference('TELEMETRY_PUBLISHABLE_KEY', $publishableKey); + + FlashMessages::addMessage(I18N::translate('The telemetry settings have been updated.'), 'success'); + + return redirect(route(ControlPanel::class)); + } +} diff --git a/app/Http/RequestHandlers/TelemetrySettingsPage.php b/app/Http/RequestHandlers/TelemetrySettingsPage.php new file mode 100644 index 00000000000..a91af3b3f1f --- /dev/null +++ b/app/Http/RequestHandlers/TelemetrySettingsPage.php @@ -0,0 +1,45 @@ +. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Http\RequestHandlers; + +use Fisharebest\Webtrees\Http\ViewResponseTrait; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Site; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +final class TelemetrySettingsPage implements RequestHandlerInterface +{ + use ViewResponseTrait; + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->layout = 'layouts/administration'; + + $title = I18N::translate('Telemetry settings'); + + return $this->viewResponse('admin/telemetry-settings', [ + 'title' => $title, + 'endpoint_url' => Site::getPreference('TELEMETRY_ENDPOINT_URL') ?: 'https://vjdwtasmykejamkdprro.supabase.co', + 'publishable_key' => Site::getPreference('TELEMETRY_PUBLISHABLE_KEY') ?: 'sb_publishable_mzCwYJ8Fs2yjE2gvQgDDeg_Vwpj5uDQ', + ]); + } +} diff --git a/app/Http/RequestHandlers/TelemetrySubmitAction.php b/app/Http/RequestHandlers/TelemetrySubmitAction.php new file mode 100644 index 00000000000..4b4047f4d05 --- /dev/null +++ b/app/Http/RequestHandlers/TelemetrySubmitAction.php @@ -0,0 +1,78 @@ +. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Http\RequestHandlers; + +use Fisharebest\Webtrees\FlashMessages; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Services\TelemetryDataService; +use Fisharebest\Webtrees\Site; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +use function json_encode; +use function redirect; +use function route; + +use const JSON_UNESCAPED_SLASHES; +use const JSON_UNESCAPED_UNICODE; + +final class TelemetrySubmitAction implements RequestHandlerInterface +{ + public function __construct( + private readonly TelemetryDataService $telemetry_data_service, + ) { + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $payload = $this->telemetry_data_service->assemblePayload(); + $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + + $endpointUrl = Site::getPreference('TELEMETRY_ENDPOINT_URL') ?: 'https://vjdwtasmykejamkdprro.supabase.co'; + $publishableKey = Site::getPreference('TELEMETRY_PUBLISHABLE_KEY') ?: 'sb_publishable_mzCwYJ8Fs2yjE2gvQgDDeg_Vwpj5uDQ'; + + $url = rtrim($endpointUrl, '/') . '/rest/v1/telemetry'; + + try { + $client = new Client([ + 'timeout' => 15, + ]); + + $client->post($url, [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'apikey' => $publishableKey, + 'Authorization' => 'Bearer ' . $publishableKey, + 'Prefer' => 'return=minimal', + ], + 'body' => $json, + ]); + + FlashMessages::addMessage(I18N::translate('Telemetry data has been sent successfully. Thank you!'), 'success'); + } catch (GuzzleException $exception) { + FlashMessages::addMessage(I18N::translate('Failed to send telemetry data.') . ' ' . $exception->getMessage(), 'danger'); + } + + return redirect(route(ControlPanel::class)); + } +} diff --git a/app/Http/Routes/WebRoutes.php b/app/Http/Routes/WebRoutes.php index d0d8825958d..cde0722fcac 100644 --- a/app/Http/Routes/WebRoutes.php +++ b/app/Http/Routes/WebRoutes.php @@ -280,6 +280,10 @@ use Fisharebest\Webtrees\Http\RequestHandlers\SubmissionPage; use Fisharebest\Webtrees\Http\RequestHandlers\SubmitterPage; use Fisharebest\Webtrees\Http\RequestHandlers\SynchronizeTrees; +use Fisharebest\Webtrees\Http\RequestHandlers\TelemetryPreviewPage; +use Fisharebest\Webtrees\Http\RequestHandlers\TelemetrySettingsAction; +use Fisharebest\Webtrees\Http\RequestHandlers\TelemetrySettingsPage; +use Fisharebest\Webtrees\Http\RequestHandlers\TelemetrySubmitAction; use Fisharebest\Webtrees\Http\RequestHandlers\TomSelectFamily; use Fisharebest\Webtrees\Http\RequestHandlers\TomSelectIndividual; use Fisharebest\Webtrees\Http\RequestHandlers\TomSelectLocation; @@ -445,6 +449,10 @@ public function load(Map $router): void $router->post(SiteRegistrationAction::class, '/site-registration'); $router->get(SiteTagsPage::class, '/tags'); $router->post(SiteTagsAction::class, '/tags'); + $router->get(TelemetryPreviewPage::class, '/telemetry-preview'); + $router->post(TelemetrySubmitAction::class, '/telemetry-submit'); + $router->get(TelemetrySettingsPage::class, '/telemetry-settings'); + $router->post(TelemetrySettingsAction::class, '/telemetry-settings'); $router->get(TreePageDefaultEdit::class, '/trees/default-blocks'); $router->post(TreePageDefaultUpdate::class, '/trees/default-blocks'); $router->get(MergeTreesPage::class, '/trees/merge'); diff --git a/app/Services/TelemetryDataService.php b/app/Services/TelemetryDataService.php new file mode 100644 index 00000000000..67692768b26 --- /dev/null +++ b/app/Services/TelemetryDataService.php @@ -0,0 +1,191 @@ +. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Services; + +use Fisharebest\Webtrees\Contracts\UserInterface; +use Fisharebest\Webtrees\DB; +use Fisharebest\Webtrees\Module\ModuleCustomInterface; +use Fisharebest\Webtrees\Module\ModuleInterface; +use Fisharebest\Webtrees\Site; +use Fisharebest\Webtrees\Webtrees; +use Illuminate\Database\Query\Expression; +use Illuminate\Support\Collection; +use PDO; + +use function hash; +use function ini_get; +use function php_uname; + +/** + * Assemble telemetry data payload for submission. + */ +class TelemetryDataService +{ + public function __construct( + private readonly ModuleService $module_service, + private readonly TreeService $tree_service, + private readonly UserService $user_service, + ) { + } + + /** + * Assemble the complete telemetry payload as an associative array. + * + * @return array + */ + public function assemblePayload(): array + { + return [ + 'p_site_uuid' => $this->siteUuid(), + 'p_metrics' => $this->metrics(), + ]; + } + + private function siteUuid(): string + { + $siteId = Site::getPreference('SITE_UUID'); + + if ($siteId === '') { + $siteId = bin2hex(random_bytes(16)); + Site::setPreference('SITE_UUID', $siteId); + } + + return hash('sha256', $siteId); + } + + /** + * @return array + */ + private function metrics(): array + { + return [ + 'php_version' => PHP_VERSION, + 'php_memory_limit' => ini_get('memory_limit') ?: 'unknown', + 'db_type' => DB::driverName(), + 'db_version' => $this->databaseVersion(), + 'os_type' => php_uname('s'), + 'webtrees_version' => Webtrees::VERSION, + 'default_language' => Site::getPreference('LANGUAGE'), + 'changes_count' => $this->changesCount(), + 'trees' => $this->treesData(), + 'default_theme' => Site::getPreference('THEME_DIR'), + 'media_count' => $this->mediaCount(), + 'users_count' => $this->user_service->all()->count(), + 'user_settings' => $this->userSettings(), + 'enabled_standard_modules' => $this->enabledStandardModules(), + 'custom_modules' => $this->customModules(), + ]; + } + + private function databaseVersion(): string + { + return DB::connection()->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + private function changesCount(): int + { + return (int) DB::table('change') + ->where('status', '=', 'pending') + ->count(); + } + + /** + * @return array> + */ + private function treesData(): array + { + $trees = $this->tree_service->all(); + $data = []; + + foreach ($trees as $tree) { + $treeId = $tree->id(); + + $data[] = [ + 'individuals_count' => (int) DB::table('individuals')->where('i_file', '=', $treeId)->count(), + 'families_count' => (int) DB::table('families')->where('f_file', '=', $treeId)->count(), + 'sources_count' => (int) DB::table('sources')->where('s_file', '=', $treeId)->count(), + 'repositories_count' => (int) DB::table('other')->where('o_file', '=', $treeId)->where('o_type', '=', 'REPO')->count(), + 'notes_count' => (int) DB::table('other')->where('o_file', '=', $treeId)->where('o_type', '=', 'NOTE')->count(), + 'places_count' => (int) DB::table('places')->where('p_file', '=', $treeId)->count(), + ]; + } + + return $data; + } + + private function mediaCount(): int + { + return (int) DB::table('media')->count(); + } + + /** + * @return array> + */ + private function userSettings(): array + { + $users = $this->user_service->all(); + $now = time(); + $settings = []; + + foreach ($users as $user) { + $registeredTimestamp = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED); + $lastLoginTimestamp = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE); + + $gedcomCanEdit = DB::table('user_gedcom_setting') + ->where('user_id', '=', $user->id()) + ->where('setting_name', '=', UserInterface::PREF_TREE_ROLE) + ->pluck('setting_value') + ->all(); + + $settings[] = [ + 'language' => $user->getPreference(UserInterface::PREF_LANGUAGE), + 'theme' => $user->getPreference(UserInterface::PREF_THEME), + 'gedcom_canedit' => $gedcomCanEdit, + 'last_login_age_days' => $lastLoginTimestamp > 0 ? (int) (($now - $lastLoginTimestamp) / 86400) : -1, + 'account_age_days' => $registeredTimestamp > 0 ? (int) (($now - $registeredTimestamp) / 86400) : -1, + ]; + } + + return $settings; + } + + /** + * @return array + */ + private function enabledStandardModules(): array + { + return $this->module_service->all() + ->filter(static fn (ModuleInterface $module): bool => !$module instanceof ModuleCustomInterface) + ->map(static fn (ModuleInterface $module): string => $module->name()) + ->values() + ->all(); + } + + /** + * @return array + */ + private function customModules(): array + { + return $this->module_service->findByInterface(ModuleCustomInterface::class) + ->map(static fn (ModuleCustomInterface $module): string => $module->name()) + ->values() + ->all(); + } +} diff --git a/resources/views/admin/control-panel.phtml b/resources/views/admin/control-panel.phtml index 29278f9006e..25e6a138fe7 100644 --- a/resources/views/admin/control-panel.phtml +++ b/resources/views/admin/control-panel.phtml @@ -39,6 +39,8 @@ use Fisharebest\Webtrees\Http\RequestHandlers\SiteLogsPage; use Fisharebest\Webtrees\Http\RequestHandlers\SitePreferencesPage; use Fisharebest\Webtrees\Http\RequestHandlers\SiteRegistrationPage; use Fisharebest\Webtrees\Http\RequestHandlers\SiteTagsPage; +use Fisharebest\Webtrees\Http\RequestHandlers\TelemetryPreviewPage; +use Fisharebest\Webtrees\Http\RequestHandlers\TelemetrySettingsPage; use Fisharebest\Webtrees\Http\RequestHandlers\TreePageDefaultEdit; use Fisharebest\Webtrees\Http\RequestHandlers\UpgradeWizardPage; use Fisharebest\Webtrees\Http\RequestHandlers\UploadMediaPage; @@ -218,6 +220,13 @@ use Illuminate\Support\Collection;

+

+ + + + +

+
@@ -284,6 +293,12 @@ use Illuminate\Support\Collection; +
  • + + + + +
  • diff --git a/resources/views/admin/telemetry-preview.phtml b/resources/views/admin/telemetry-preview.phtml new file mode 100644 index 00000000000..0cb337e4539 --- /dev/null +++ b/resources/views/admin/telemetry-preview.phtml @@ -0,0 +1,46 @@ + + + [route(ControlPanel::class) => I18N::translate('Control panel'), $title]]) ?> + +

    + +

    + +

    + +
    +
    +
    +
    +
    + +
    + + +
    +
    + + + + + + +
    +
    +
    diff --git a/resources/views/admin/telemetry-settings.phtml b/resources/views/admin/telemetry-settings.phtml new file mode 100644 index 00000000000..b7180aa24bc --- /dev/null +++ b/resources/views/admin/telemetry-settings.phtml @@ -0,0 +1,61 @@ + + + [route(ControlPanel::class) => I18N::translate('Control panel'), $title]]) ?> + +

    + +
    +
    + +
    + +
    + +
    +
    +
    + +
    + +
    + +
    + +
    +
    +
    + +
    +
    + + + + + + +
    +
    + + +
    diff --git a/resources/views/icons/telemetry.phtml b/resources/views/icons/telemetry.phtml new file mode 100644 index 00000000000..be8d912bf4a --- /dev/null +++ b/resources/views/icons/telemetry.phtml @@ -0,0 +1 @@ + Date: Fri, 22 May 2026 20:56:50 +0200 Subject: [PATCH 02/11] telemetry: fixed rpc call url and do not truncate response in case of error --- app/Http/RequestHandlers/TelemetrySubmitAction.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Http/RequestHandlers/TelemetrySubmitAction.php b/app/Http/RequestHandlers/TelemetrySubmitAction.php index 4b4047f4d05..c7feb3c6d4f 100644 --- a/app/Http/RequestHandlers/TelemetrySubmitAction.php +++ b/app/Http/RequestHandlers/TelemetrySubmitAction.php @@ -25,6 +25,7 @@ use Fisharebest\Webtrees\Site; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Exception\RequestException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -51,7 +52,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $endpointUrl = Site::getPreference('TELEMETRY_ENDPOINT_URL') ?: 'https://vjdwtasmykejamkdprro.supabase.co'; $publishableKey = Site::getPreference('TELEMETRY_PUBLISHABLE_KEY') ?: 'sb_publishable_mzCwYJ8Fs2yjE2gvQgDDeg_Vwpj5uDQ'; - $url = rtrim($endpointUrl, '/') . '/rest/v1/telemetry'; + $url = rtrim($endpointUrl, '/') . '/rest/v1/rpc/submit_webtrees_telemetry'; try { $client = new Client([ @@ -70,7 +71,14 @@ public function handle(ServerRequestInterface $request): ResponseInterface FlashMessages::addMessage(I18N::translate('Telemetry data has been sent successfully. Thank you!'), 'success'); } catch (GuzzleException $exception) { - FlashMessages::addMessage(I18N::translate('Failed to send telemetry data.') . ' ' . $exception->getMessage(), 'danger'); + $errorDetail = ''; + if ($exception instanceof RequestException && $exception->getResponse() !== null) { + $errorDetail = (string) $exception->getResponse()->getBody(); + } + if ($errorDetail === '') { + $errorDetail = $exception->getMessage(); + } + FlashMessages::addMessage(I18N::translate('Failed to send telemetry data.') . ' ' . $errorDetail, 'danger'); } return redirect(route(ControlPanel::class)); From 217b504712ed2b0d70b28568e30bb3d1127df49f Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 21:00:43 +0200 Subject: [PATCH 03/11] telemetry: count total changes --- app/Services/TelemetryDataService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Services/TelemetryDataService.php b/app/Services/TelemetryDataService.php index 67692768b26..021a54a7f4e 100644 --- a/app/Services/TelemetryDataService.php +++ b/app/Services/TelemetryDataService.php @@ -102,7 +102,6 @@ private function databaseVersion(): string private function changesCount(): int { return (int) DB::table('change') - ->where('status', '=', 'pending') ->count(); } From 99af063b5551d65f6c405b9e76af3eaa56d676b8 Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 21:00:59 +0200 Subject: [PATCH 04/11] telemetry: send data lower case as check now --- resources/views/admin/control-panel.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/admin/control-panel.phtml b/resources/views/admin/control-panel.phtml index 25e6a138fe7..fba1e10bf39 100644 --- a/resources/views/admin/control-panel.phtml +++ b/resources/views/admin/control-panel.phtml @@ -223,7 +223,7 @@ use Illuminate\Support\Collection;

    - +

    From c8846bd4101058cf200f2a4592b2e39a8de716dc Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 21:06:40 +0200 Subject: [PATCH 05/11] telemetry: refactored a unique Site::getUuid() for both UpgradeService and TelemetryDataService --- app/Services/TelemetryDataService.php | 11 +---------- app/Services/UpgradeService.php | 9 +-------- app/Site.php | 19 +++++++++++++++++-- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/app/Services/TelemetryDataService.php b/app/Services/TelemetryDataService.php index 021a54a7f4e..42ef667cb6b 100644 --- a/app/Services/TelemetryDataService.php +++ b/app/Services/TelemetryDataService.php @@ -25,11 +25,9 @@ use Fisharebest\Webtrees\Module\ModuleInterface; use Fisharebest\Webtrees\Site; use Fisharebest\Webtrees\Webtrees; -use Illuminate\Database\Query\Expression; use Illuminate\Support\Collection; use PDO; -use function hash; use function ini_get; use function php_uname; @@ -60,14 +58,7 @@ public function assemblePayload(): array private function siteUuid(): string { - $siteId = Site::getPreference('SITE_UUID'); - - if ($siteId === '') { - $siteId = bin2hex(random_bytes(16)); - Site::setPreference('SITE_UUID', $siteId); - } - - return hash('sha256', $siteId); + return Site::getUuid(); } /** diff --git a/app/Services/UpgradeService.php b/app/Services/UpgradeService.php index d834e3667aa..3b1d95eb659 100644 --- a/app/Services/UpgradeService.php +++ b/app/Services/UpgradeService.php @@ -344,17 +344,10 @@ private function fetchLatestVersion(bool $force): string */ private function serverParameters(): array { - $site_uuid = Site::getPreference('SITE_UUID'); - - if ($site_uuid === '') { - $site_uuid = Registry::idFactory()->uuid(); - Site::setPreference('SITE_UUID', $site_uuid); - } - return [ 'w' => Webtrees::VERSION, 'p' => PHP_VERSION, - 's' => $site_uuid, + 's' => Site::getUuid(), 'd' => DB::driverName(), ]; } diff --git a/app/Site.php b/app/Site.php index b8ba9d0ccc6..ddb23abf984 100644 --- a/app/Site.php +++ b/app/Site.php @@ -135,8 +135,23 @@ public static function setPreference(string $setting_name, string $setting_value } } - /** - * Get the site’s configuration settings + /** * Get or generate the unique site identifier. + * + * @return string + */ + public static function getUuid(): string + { + $site_uuid = self::getPreference('SITE_UUID'); + + if ($site_uuid === '') { + $site_uuid = Registry::idFactory()->uuid(); + self::setPreference('SITE_UUID', $site_uuid); + } + + return $site_uuid; + } + + /** * Get the site’s configuration settings * * @param string $setting_name * From a9b6d8f4ea2126ae07c1a45f52899beee71e901d Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 21:27:29 +0200 Subject: [PATCH 06/11] telemetry: use existing font awesome icon --- resources/views/icons/telemetry.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/icons/telemetry.phtml b/resources/views/icons/telemetry.phtml index be8d912bf4a..b7dc338953b 100644 --- a/resources/views/icons/telemetry.phtml +++ b/resources/views/icons/telemetry.phtml @@ -1 +1 @@ - Date: Fri, 22 May 2026 21:28:20 +0200 Subject: [PATCH 07/11] telemetry: extended disclosure and move action buttons above sample --- resources/views/admin/telemetry-preview.phtml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/resources/views/admin/telemetry-preview.phtml b/resources/views/admin/telemetry-preview.phtml index 0cb337e4539..3daac03e674 100644 --- a/resources/views/admin/telemetry-preview.phtml +++ b/resources/views/admin/telemetry-preview.phtml @@ -18,15 +18,9 @@ use Fisharebest\Webtrees\I18N;

    - +

    -
    -
    -
    -
    -
    -
    @@ -44,3 +38,10 @@ use Fisharebest\Webtrees\I18N;
    + +
    +
    +
    +
    +
    + From 85bbfced429918a641286deb31bbb5597edf6916 Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 21:30:59 +0200 Subject: [PATCH 08/11] telemetry: media per tree instead of total --- app/Services/TelemetryDataService.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/Services/TelemetryDataService.php b/app/Services/TelemetryDataService.php index 42ef667cb6b..6b5841e4386 100644 --- a/app/Services/TelemetryDataService.php +++ b/app/Services/TelemetryDataService.php @@ -77,7 +77,6 @@ private function metrics(): array 'changes_count' => $this->changesCount(), 'trees' => $this->treesData(), 'default_theme' => Site::getPreference('THEME_DIR'), - 'media_count' => $this->mediaCount(), 'users_count' => $this->user_service->all()->count(), 'user_settings' => $this->userSettings(), 'enabled_standard_modules' => $this->enabledStandardModules(), @@ -112,6 +111,7 @@ private function treesData(): array 'families_count' => (int) DB::table('families')->where('f_file', '=', $treeId)->count(), 'sources_count' => (int) DB::table('sources')->where('s_file', '=', $treeId)->count(), 'repositories_count' => (int) DB::table('other')->where('o_file', '=', $treeId)->where('o_type', '=', 'REPO')->count(), + 'media_count' => (int) DB::table('media')->where('m_file', '=', $treeId)->count(), 'notes_count' => (int) DB::table('other')->where('o_file', '=', $treeId)->where('o_type', '=', 'NOTE')->count(), 'places_count' => (int) DB::table('places')->where('p_file', '=', $treeId)->count(), ]; @@ -120,11 +120,6 @@ private function treesData(): array return $data; } - private function mediaCount(): int - { - return (int) DB::table('media')->count(); - } - /** * @return array> */ From b4533a0ef1f33ce1b158f791ec4031de34ae5726 Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 21:35:31 +0200 Subject: [PATCH 09/11] telemetry: user permissions unrolled in the tree statistics, removed user theme preference --- app/Services/TelemetryDataService.php | 32 ++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/app/Services/TelemetryDataService.php b/app/Services/TelemetryDataService.php index 6b5841e4386..ba9462e3e2c 100644 --- a/app/Services/TelemetryDataService.php +++ b/app/Services/TelemetryDataService.php @@ -106,14 +106,24 @@ private function treesData(): array foreach ($trees as $tree) { $treeId = $tree->id(); + $roleCounts = DB::table('user_gedcom_setting') + ->where('gedcom_id', '=', $treeId) + ->where('setting_name', '=', UserInterface::PREF_TREE_ROLE) + ->selectRaw('setting_value, COUNT(*) as total') + ->groupBy('setting_value') + ->pluck('total', 'setting_value') + ->map(static fn (string $count): int => (int) $count) + ->all(); + $data[] = [ - 'individuals_count' => (int) DB::table('individuals')->where('i_file', '=', $treeId)->count(), - 'families_count' => (int) DB::table('families')->where('f_file', '=', $treeId)->count(), - 'sources_count' => (int) DB::table('sources')->where('s_file', '=', $treeId)->count(), - 'repositories_count' => (int) DB::table('other')->where('o_file', '=', $treeId)->where('o_type', '=', 'REPO')->count(), - 'media_count' => (int) DB::table('media')->where('m_file', '=', $treeId)->count(), - 'notes_count' => (int) DB::table('other')->where('o_file', '=', $treeId)->where('o_type', '=', 'NOTE')->count(), - 'places_count' => (int) DB::table('places')->where('p_file', '=', $treeId)->count(), + 'individuals_count' => (int) DB::table('individuals')->where('i_file', '=', $treeId)->count(), + 'families_count' => (int) DB::table('families')->where('f_file', '=', $treeId)->count(), + 'sources_count' => (int) DB::table('sources')->where('s_file', '=', $treeId)->count(), + 'repositories_count' => (int) DB::table('other')->where('o_file', '=', $treeId)->where('o_type', '=', 'REPO')->count(), + 'media_count' => (int) DB::table('media')->where('m_file', '=', $treeId)->count(), + 'notes_count' => (int) DB::table('other')->where('o_file', '=', $treeId)->where('o_type', '=', 'NOTE')->count(), + 'places_count' => (int) DB::table('places')->where('p_file', '=', $treeId)->count(), + 'user_permissions_count' => $roleCounts, ]; } @@ -133,16 +143,8 @@ private function userSettings(): array $registeredTimestamp = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_REGISTERED); $lastLoginTimestamp = (int) $user->getPreference(UserInterface::PREF_TIMESTAMP_ACTIVE); - $gedcomCanEdit = DB::table('user_gedcom_setting') - ->where('user_id', '=', $user->id()) - ->where('setting_name', '=', UserInterface::PREF_TREE_ROLE) - ->pluck('setting_value') - ->all(); - $settings[] = [ 'language' => $user->getPreference(UserInterface::PREF_LANGUAGE), - 'theme' => $user->getPreference(UserInterface::PREF_THEME), - 'gedcom_canedit' => $gedcomCanEdit, 'last_login_age_days' => $lastLoginTimestamp > 0 ? (int) (($now - $lastLoginTimestamp) / 86400) : -1, 'account_age_days' => $registeredTimestamp > 0 ? (int) (($now - $registeredTimestamp) / 86400) : -1, ]; From 91fc0fe4870b9cdd047227ee85151ff926aa67bb Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 21:52:18 +0200 Subject: [PATCH 10/11] telemetry: module name is the folder name, that is arbitrary, use module title in English --- app/Services/TelemetryDataService.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/Services/TelemetryDataService.php b/app/Services/TelemetryDataService.php index ba9462e3e2c..495dde194d9 100644 --- a/app/Services/TelemetryDataService.php +++ b/app/Services/TelemetryDataService.php @@ -21,6 +21,7 @@ use Fisharebest\Webtrees\Contracts\UserInterface; use Fisharebest\Webtrees\DB; +use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Module\ModuleCustomInterface; use Fisharebest\Webtrees\Module\ModuleInterface; use Fisharebest\Webtrees\Site; @@ -170,9 +171,16 @@ private function enabledStandardModules(): array */ private function customModules(): array { - return $this->module_service->findByInterface(ModuleCustomInterface::class) - ->map(static fn (ModuleCustomInterface $module): string => $module->name()) + $currentLanguage = I18N::languageTag(); + I18N::init('en-US'); + + $modules = $this->module_service->findByInterface(ModuleCustomInterface::class) + ->map(static fn (ModuleCustomInterface $module): string => $module->title()) ->values() ->all(); + + I18N::init($currentLanguage); + + return $modules; } } From 308cc63291984d549626672d25849b0a56ab7e56 Mon Sep 17 00:00:00 2001 From: Giulio Marcon Date: Fri, 22 May 2026 22:16:17 +0200 Subject: [PATCH 11/11] telemetry: better to use module namespace instead of English title --- app/Services/TelemetryDataService.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/Services/TelemetryDataService.php b/app/Services/TelemetryDataService.php index 495dde194d9..b23084aab8c 100644 --- a/app/Services/TelemetryDataService.php +++ b/app/Services/TelemetryDataService.php @@ -21,7 +21,6 @@ use Fisharebest\Webtrees\Contracts\UserInterface; use Fisharebest\Webtrees\DB; -use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Module\ModuleCustomInterface; use Fisharebest\Webtrees\Module\ModuleInterface; use Fisharebest\Webtrees\Site; @@ -171,16 +170,9 @@ private function enabledStandardModules(): array */ private function customModules(): array { - $currentLanguage = I18N::languageTag(); - I18N::init('en-US'); - - $modules = $this->module_service->findByInterface(ModuleCustomInterface::class) - ->map(static fn (ModuleCustomInterface $module): string => $module->title()) + return $this->module_service->findByInterface(ModuleCustomInterface::class) + ->map(static fn (ModuleCustomInterface $module): string => $module::class) ->values() ->all(); - - I18N::init($currentLanguage); - - return $modules; } }