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;
+
+ = I18N::translate('Please send telemetry data to support development, you will see the submitted data before confirming.') ?>
+
+ = I18N::translate('Send data') ?>
+
+
+
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 @@
+
+
+= view('components/breadcrumbs', ['links' => [route(ControlPanel::class) => I18N::translate('Control panel'), $title]]) ?>
+
+= $title ?>
+
+
+ = I18N::translate('Please review the telemetry data below. Click "Confirm" to send this data, or "Cancel" to return to the control panel.') ?>
+
+
+
+
+
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 @@
+
+
+= view('components/breadcrumbs', ['links' => [route(ControlPanel::class) => I18N::translate('Control panel'), $title]]) ?>
+
+= $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;
= I18N::translate('Please send telemetry data to support development, you will see the submitted data before confirming.') ?>
- = I18N::translate('Send data') ?>
+ = I18N::translate('send data') ?>
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;
= $title ?>
- = I18N::translate('Please review the telemetry data below. Click "Confirm" to send this data, or "Cancel" to return to the control panel.') ?>
+ = I18N::translate('Thank you for supporting the development of webtrees! Please review the telemetry data that will be submitted below. Click "Confirm" to send this data, or "Cancel" to return to the control panel.') ?>
-
-
+
+
+
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;
}
}