From 7578b50b4bd21c664c9dc8bffc618591b667f949 Mon Sep 17 00:00:00 2001 From: Alexis Guyomar Date: Tue, 3 Dec 2024 10:41:32 +0100 Subject: [PATCH 01/18] feat: add url utils --- _dev/src/ts/utils/urlUtils.ts | 8 ++++ _dev/tests/utils/urlUtils.test.ts | 70 +++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 _dev/src/ts/utils/urlUtils.ts create mode 100644 _dev/tests/utils/urlUtils.test.ts diff --git a/_dev/src/ts/utils/urlUtils.ts b/_dev/src/ts/utils/urlUtils.ts new file mode 100644 index 000000000..85a436fc4 --- /dev/null +++ b/_dev/src/ts/utils/urlUtils.ts @@ -0,0 +1,8 @@ +export function maskSensitiveInfoInUrl(url: string, adminFolder: string): string { + const placeHolder = '********'; + const adminFolderRegex = new RegExp(adminFolder, 'g'); + const maskedUrl = url.replace(adminFolderRegex, placeHolder); + + const tokenRegex = new RegExp('&token=[^&]*', 'gi'); + return maskedUrl.replace(tokenRegex, `&token=${placeHolder}`); +} diff --git a/_dev/tests/utils/urlUtils.test.ts b/_dev/tests/utils/urlUtils.test.ts new file mode 100644 index 000000000..4e13059fe --- /dev/null +++ b/_dev/tests/utils/urlUtils.test.ts @@ -0,0 +1,70 @@ +import { maskSensitiveInfoInUrl } from '../../src/ts/utils/urlUtils'; + +describe('urlUtils', () => { + describe('maskSensitiveInfoInUrl', () => { + const adminFolder = 'admin-dev'; + + test('URL with admin folder and token', () => { + const url = + 'http://myshop.com/admin-dev/index.php?controller=AdminSelfUpgrade&token=831ecc0c2e1c41af40cee361afec03f3&route=home-page'; + expect(maskSensitiveInfoInUrl(url, adminFolder)).toBe( + 'http://myshop.com/********/index.php?controller=AdminSelfUpgrade&token=********&route=home-page' + ); + }); + + test('URL without token', () => { + const url = + 'http://myshop.com/admin-dev/index.php?controller=AdminSelfUpgrade&route=home-page'; + expect(maskSensitiveInfoInUrl(url, adminFolder)).toBe( + 'http://myshop.com/********/index.php?controller=AdminSelfUpgrade&route=home-page' + ); + }); + + test('URL with token but without admin folder', () => { + const url = + 'http://myshop.com/index.php?controller=AdminSelfUpgrade&token=831ecc0c2e1c41af40cee361afec03f3&route=home-page'; + expect(maskSensitiveInfoInUrl(url, adminFolder)).toBe( + 'http://myshop.com/index.php?controller=AdminSelfUpgrade&token=********&route=home-page' + ); + }); + + test('URL without admin folder & token', () => { + const url = 'http://myshop.com/index.php?controller=AdminSelfUpgrade&route=home-page'; + expect(maskSensitiveInfoInUrl(url, adminFolder)).toBe( + 'http://myshop.com/index.php?controller=AdminSelfUpgrade&route=home-page' + ); + }); + + test('URL with multiple admin folder occurrence', () => { + const url = + 'http://myshop.com/admin-dev/admin-dev/index.php?controller=AdminSelfUpgrade&token=831ecc0c2e1c41af40cee361afec03f3&route=home-page'; + expect(maskSensitiveInfoInUrl(url, adminFolder)).toBe( + 'http://myshop.com/********/********/index.php?controller=AdminSelfUpgrade&token=********&route=home-page' + ); + }); + + test('URL with empty token', () => { + const url = + 'http://myshop.com/admin-dev/index.php?controller=AdminSelfUpgrade&token=&route=home-page'; + expect(maskSensitiveInfoInUrl(url, adminFolder)).toBe( + 'http://myshop.com/********/index.php?controller=AdminSelfUpgrade&token=********&route=home-page' + ); + }); + + test('URL with similar token parameter', () => { + const url = + 'http://myshop.com/admin-dev/index.php?controller=AdminSelfUpgrade&mytoken=12345&route=home-page'; + expect(maskSensitiveInfoInUrl(url, adminFolder)).toBe( + 'http://myshop.com/********/index.php?controller=AdminSelfUpgrade&mytoken=12345&route=home-page' + ); + }); + + test('Case sensitivity for token parameter', () => { + const url = + 'http://myshop.com/admin-dev/index.php?controller=AdminSelfUpgrade&TOKEN=831ecc0c2e1c41af40cee361afec03f3&route=home-page'; + expect(maskSensitiveInfoInUrl(url, adminFolder)).toBe( + 'http://myshop.com/********/index.php?controller=AdminSelfUpgrade&token=********&route=home-page' + ); + }); + }); +}); From 3b044fb48a93dc0781f4a739a2aa8df61221d419 Mon Sep 17 00:00:00 2001 From: Alexis Guyomar Date: Tue, 3 Dec 2024 10:46:11 +0100 Subject: [PATCH 02/18] feat: add module version to js variables --- _dev/global.d.ts | 1 + _dev/jest.setup.ts | 3 ++- controllers/admin/AdminSelfUpgradeController.php | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/_dev/global.d.ts b/_dev/global.d.ts index 668837dc1..5f93df76e 100644 --- a/_dev/global.d.ts +++ b/_dev/global.d.ts @@ -5,6 +5,7 @@ interface AutoUpgradeVariables { admin_url: string; admin_dir: string; stepper_parent_id: string; + module_version: string; } declare global { diff --git a/_dev/jest.setup.ts b/_dev/jest.setup.ts index 57a0f94d7..c65b6d285 100644 --- a/_dev/jest.setup.ts +++ b/_dev/jest.setup.ts @@ -3,7 +3,8 @@ window.AutoUpgradeVariables = { token: 'test-token', admin_url: 'http://localhost', admin_dir: '/admin_directory', - stepper_parent_id: 'stepper_content' + stepper_parent_id: 'stepper_content', + module_version: '7.1.0' }; beforeAll(() => {}); diff --git a/controllers/admin/AdminSelfUpgradeController.php b/controllers/admin/AdminSelfUpgradeController.php index d683fa994..e4e5d75f8 100644 --- a/controllers/admin/AdminSelfUpgradeController.php +++ b/controllers/admin/AdminSelfUpgradeController.php @@ -539,6 +539,7 @@ private function getScriptsVariables() 'admin_url' => __PS_BASE_URI__ . $adminDir, 'admin_dir' => $adminDir, 'stepper_parent_id' => \PrestaShop\Module\AutoUpgrade\Twig\PageSelectors::STEPPER_PARENT_ID, + 'module_version' => $this->module->version, ]; } From 7dca01fcdcb5a71e1e54678ba20c3076da7c4d93 Mon Sep 17 00:00:00 2001 From: Alexis Guyomar Date: Tue, 3 Dec 2024 10:47:28 +0100 Subject: [PATCH 03/18] feat: add sentry api --- _dev/package-lock.json | 86 +++++++++++++++++++++++++++++ _dev/package.json | 1 + _dev/src/ts/api/sentryApi.ts | 102 +++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 _dev/src/ts/api/sentryApi.ts diff --git a/_dev/package-lock.json b/_dev/package-lock.json index 6182f541f..4a741ed33 100644 --- a/_dev/package-lock.json +++ b/_dev/package-lock.json @@ -7,6 +7,7 @@ "name": "autoupgrade", "license": "AFL", "dependencies": { + "@sentry/browser": "^8.41.0", "axios": "^1.7.7" }, "devDependencies": { @@ -2244,6 +2245,91 @@ "win32" ] }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.41.0.tgz", + "integrity": "sha512-nU7Bn3jEUmf1QXRUT3j2ewUBlFJpe9vnAnjqpeVPDWTsVI52BwVNcJHuE37PrGs66OZ1ZkGMfKnQk43oCAa+oQ==", + "dependencies": { + "@sentry/core": "8.41.0", + "@sentry/types": "8.41.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.41.0.tgz", + "integrity": "sha512-bw+BrSNw8abOnu/IpD8YSbYubXkkT8jyNS7TM4e4UPZMuXcbtia7/r5d7kAiUfKv/sV5PNMlZLOk+EYJeLTANg==", + "dependencies": { + "@sentry/core": "8.41.0", + "@sentry/types": "8.41.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.41.0.tgz", + "integrity": "sha512-ByXEY7JI95y4Qr9fS3d28l9uuVU5Qa0HgL+xDmYElNx7CXz3Q9hFN6ibgUeC3h8BO5pDULxWNgAppl7FRY8N5w==", + "dependencies": { + "@sentry-internal/browser-utils": "8.41.0", + "@sentry/core": "8.41.0", + "@sentry/types": "8.41.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.41.0.tgz", + "integrity": "sha512-lpgOBHWr1ZNxidD72A2pfoUMjIpwonOPYoQZWAHr86Oa3eIVQOyfklZlHW+gKPFl2/IEl9Lbtcke0JiDp3dkIQ==", + "dependencies": { + "@sentry-internal/replay": "8.41.0", + "@sentry/core": "8.41.0", + "@sentry/types": "8.41.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.41.0.tgz", + "integrity": "sha512-FfAU55eYwW2lG4M3dEw2472RvHrD5YWSfHCZvuRf/4skX38kFvKghZQ+epL+CVHTzvIRHOrbj8qQK6YLTGl9ew==", + "dependencies": { + "@sentry-internal/browser-utils": "8.41.0", + "@sentry-internal/feedback": "8.41.0", + "@sentry-internal/replay": "8.41.0", + "@sentry-internal/replay-canvas": "8.41.0", + "@sentry/core": "8.41.0", + "@sentry/types": "8.41.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.41.0.tgz", + "integrity": "sha512-3v7u3t4LozCA5SpZY4yqUN2U3jSrkXNoLgz6L2SUUiydyCuSwXZIFEwpLJfgQyidpNDifeQbBI5E1O910XkPsA==", + "dependencies": { + "@sentry/types": "8.41.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.41.0.tgz", + "integrity": "sha512-eqdnGr9k9H++b9CjVUoTNUVahPVWeNnMy0YGkqS5+cjWWC+x43p56202oidGFmWo6702ub/xwUNH6M5PC4kq6A==", + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", diff --git a/_dev/package.json b/_dev/package.json index 5c36df58e..e33f99b83 100644 --- a/_dev/package.json +++ b/_dev/package.json @@ -48,6 +48,7 @@ "vite-plugin-static-copy": "^1.0.6" }, "dependencies": { + "@sentry/browser": "^8.41.0", "axios": "^1.7.7" } } diff --git a/_dev/src/ts/api/sentryApi.ts b/_dev/src/ts/api/sentryApi.ts new file mode 100644 index 000000000..d59720ec4 --- /dev/null +++ b/_dev/src/ts/api/sentryApi.ts @@ -0,0 +1,102 @@ +import * as Sentry from '@sentry/browser'; +import { SeverityLevel } from '@sentry/browser'; +import { maskSensitiveInfoInUrl } from '../utils/urlUtils'; + +Sentry.init({ + dsn: 'https://eae192966a8d79509154c65c317a7e5d@o298402.ingest.us.sentry.io/4507254110552064', + release: `v${window.AutoUpgradeVariables.module_version}`, + sendDefaultPii: false, + beforeSend(event) { + if (event.type === 'session') { + return null; + } + + if (event.request?.url) { + event.request.url = maskSensitiveInfoInUrl( + window.location.href, + window.AutoUpgradeVariables.admin_dir + ); + } + + return event; + }, + beforeBreadcrumb(breadcrumb) { + if (breadcrumb.data?.url) { + breadcrumb.data.url = maskSensitiveInfoInUrl( + breadcrumb.data.url, + window.AutoUpgradeVariables.admin_dir + ); + } + + if (breadcrumb.data?.from) { + breadcrumb.data.from = maskSensitiveInfoInUrl( + breadcrumb.data.from, + window.AutoUpgradeVariables.admin_dir + ); + } + + if (breadcrumb.data?.to) { + breadcrumb.data.to = maskSensitiveInfoInUrl( + breadcrumb.data.to, + window.AutoUpgradeVariables.admin_dir + ); + } + + return breadcrumb; + } +}); + +export function sendUserFeedback( + message: string, + logs: { logs?: string; warnings?: string; errors?: string } = {}, + feedback: { email?: string; comments?: string } = {}, + level: SeverityLevel = 'error' +) { + if (logs.logs) { + Sentry.getCurrentScope().addAttachment({ + filename: 'logs.txt', + data: logs.logs, + contentType: 'text/plain' + }); + } + + if (logs.warnings) { + Sentry.getCurrentScope().addAttachment({ + filename: 'summary_warnings.txt', + data: logs.warnings, + contentType: 'text/plain' + }); + } + + if (logs.errors) { + Sentry.getCurrentScope().addAttachment({ + filename: 'summary_errors.txt', + data: logs.errors, + contentType: 'text/plain' + }); + } + + const maskedUrl = maskSensitiveInfoInUrl( + window.location.href, + window.AutoUpgradeVariables.admin_dir + ); + + const eventId = Sentry.captureEvent({ + message, + level, + tags: { + url: maskedUrl + } + }); + + if (feedback.email || feedback.comments) { + Sentry.captureFeedback({ + associatedEventId: eventId, + email: feedback.email || '', + message: feedback.comments || '', + url: maskedUrl + }); + } + + Sentry.getCurrentScope().clearAttachments(); +} From aef68d82be51910c53ea8da992ff89f6ce4f604a Mon Sep 17 00:00:00 2001 From: Alexis Guyomar Date: Thu, 5 Dec 2024 10:50:30 +0100 Subject: [PATCH 04/18] feat: add data transparency link --- classes/Commands/UpdateCommand.php | 6 +++--- .../{DeveloperDocumentation.php => DocumentationLinks.php} | 4 +++- controllers/admin/self-managed/AbstractPageController.php | 2 ++ views/templates/components/privacy.html.twig | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) rename classes/{DeveloperDocumentation.php => DocumentationLinks.php} (62%) diff --git a/classes/Commands/UpdateCommand.php b/classes/Commands/UpdateCommand.php index 1e9af570f..fd431264a 100644 --- a/classes/Commands/UpdateCommand.php +++ b/classes/Commands/UpdateCommand.php @@ -29,7 +29,7 @@ use Exception; use InvalidArgumentException; -use PrestaShop\Module\AutoUpgrade\DeveloperDocumentation; +use PrestaShop\Module\AutoUpgrade\DocumentationLinks; use PrestaShop\Module\AutoUpgrade\Parameters\UpgradeConfiguration; use PrestaShop\Module\AutoUpgrade\Parameters\UpgradeFileNames; use PrestaShop\Module\AutoUpgrade\Task\ExitCode; @@ -53,7 +53,7 @@ protected function configure(): void ->setDescription('Update your store.') ->setHelp( 'This command allows you to start the update process. ' . - 'Advanced users can refer to the ' . DeveloperDocumentation::DEV_DOC_UPGRADE_CLI_URL . ' for further details on available actions' + 'Advanced users can refer to the ' . DocumentationLinks::DEV_DOC_UPGRADE_CLI_URL . ' for further details on available actions' ) ->addArgument('admin-dir', InputArgument::REQUIRED, 'The admin directory name.') ->addOption('chain', null, InputOption::VALUE_NONE, 'True by default. Allows you to chain update commands automatically. The command will continue executing subsequent tasks without requiring manual intervention to restart the process.') @@ -65,7 +65,7 @@ protected function configure(): void ->addOption('regenerate-email-templates', null, InputOption::VALUE_REQUIRED, "Regenerate email templates. If you've customized email templates, your changes will be lost if you activate this option (1 for yes, 0 for no)") ->addOption('disable-all-overrides', null, InputOption::VALUE_REQUIRED, 'Overriding is a way to replace business behaviors (class files and controller files) to target only one method or as many as you need. This option disables all classes & controllers overrides, allowing you to avoid conflicts during and after updates (1 for yes, 0 for no)') ->addOption('config-file-path', null, InputOption::VALUE_REQUIRED, 'Configuration file location for update.') - ->addOption('action', null, InputOption::VALUE_REQUIRED, 'Advanced users only. Sets the step you want to start from. Only the "' . TaskName::TASK_UPDATE_INITIALIZATION . '" task updates the configuration. (Default: ' . TaskName::TASK_UPDATE_INITIALIZATION . ', see ' . DeveloperDocumentation::DEV_DOC_UPGRADE_CLI_URL . ' for other values available)'); + ->addOption('action', null, InputOption::VALUE_REQUIRED, 'Advanced users only. Sets the step you want to start from. Only the "' . TaskName::TASK_UPDATE_INITIALIZATION . '" task updates the configuration. (Default: ' . TaskName::TASK_UPDATE_INITIALIZATION . ', see ' . DocumentationLinks::DEV_DOC_UPGRADE_CLI_URL . ' for other values available)'); } /** diff --git a/classes/DeveloperDocumentation.php b/classes/DocumentationLinks.php similarity index 62% rename from classes/DeveloperDocumentation.php rename to classes/DocumentationLinks.php index 07e879aa4..9d7106a39 100644 --- a/classes/DeveloperDocumentation.php +++ b/classes/DocumentationLinks.php @@ -2,9 +2,11 @@ namespace PrestaShop\Module\AutoUpgrade; -class DeveloperDocumentation +class DocumentationLinks { public const DEV_DOC_URL = 'https://devdocs.prestashop-project.org/8'; public const DEV_DOC_UPGRADE_URL = self::DEV_DOC_URL . '/basics/keeping-up-to-date/upgrade-module'; public const DEV_DOC_UPGRADE_CLI_URL = self::DEV_DOC_UPGRADE_URL . '/basics/keeping-up-to-date/upgrade-module/upgrade-cli'; + public const PRESTASHOP_PROJECT_URL = 'https://www.prestashop-project.org'; + public const PRESTASHOP_PROJECT_DATA_TRANSPARENCY_URL = self::PRESTASHOP_PROJECT_URL . '/data-transparency'; } diff --git a/controllers/admin/self-managed/AbstractPageController.php b/controllers/admin/self-managed/AbstractPageController.php index f6fb6445a..f969d9d96 100644 --- a/controllers/admin/self-managed/AbstractPageController.php +++ b/controllers/admin/self-managed/AbstractPageController.php @@ -28,6 +28,7 @@ namespace PrestaShop\Module\AutoUpgrade\Controller; use PrestaShop\Module\AutoUpgrade\AjaxResponseBuilder; +use PrestaShop\Module\AutoUpgrade\DocumentationLinks; use PrestaShop\Module\AutoUpgrade\Twig\PageSelectors; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -62,6 +63,7 @@ public function renderPage(string $page, array $params): string [ 'page' => $page, 'ps_version' => $this->getPsVersionClass(), + 'data_transparency_link' => DocumentationLinks::PRESTASHOP_PROJECT_DATA_TRANSPARENCY_URL, ], $pageSelectors::getAllSelectors(), $params diff --git a/views/templates/components/privacy.html.twig b/views/templates/components/privacy.html.twig index d335f1a64..fa715cda2 100644 --- a/views/templates/components/privacy.html.twig +++ b/views/templates/components/privacy.html.twig @@ -1,6 +1,6 @@ {{ 'Privacy policy'|trans({}) }} From 28ba32ec425339a67fe98d72ca79be9598ed2373 Mon Sep 17 00:00:00 2001 From: Alexis Guyomar Date: Thu, 5 Dec 2024 10:51:01 +0100 Subject: [PATCH 05/18] feat: add reusable template for error report --- classes/Router/Router.php | 4 ++ classes/Router/Routes.php | 1 + .../Traits/displayErrorReportDialogTrait.php | 52 +++++++++++++++++++ .../UpdatePageUpdateController.php | 10 ++-- .../dialogs/dialog-error-report.html.twig | 30 ++++++----- 5 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 classes/Traits/displayErrorReportDialogTrait.php diff --git a/classes/Router/Router.php b/classes/Router/Router.php index 9cd180114..c04d26823 100644 --- a/classes/Router/Router.php +++ b/classes/Router/Router.php @@ -126,6 +126,10 @@ public function __construct(UpgradeContainer $upgradeContainer) 'controller' => UpdatePageUpdateController::class, 'method' => 'step', ], + Routes::UPDATE_STEP_UPDATE_SUBMIT_ERROR_REPORT => [ + 'controller' => UpdatePageUpdateController::class, + 'method' => 'submitErrorReport', + ], Routes::UPDATE_STEP_UPDATE_DOWNLOAD_LOGS => [ 'controller' => UpdatePageUpdateController::class, 'method' => 'getDownloadLogsButton', diff --git a/classes/Router/Routes.php b/classes/Router/Routes.php index 181058d8b..77de50076 100644 --- a/classes/Router/Routes.php +++ b/classes/Router/Routes.php @@ -33,6 +33,7 @@ class Routes /* step: update */ const UPDATE_PAGE_UPDATE = 'update-page-update'; const UPDATE_STEP_UPDATE = 'update-step-update'; + const UPDATE_STEP_UPDATE_SUBMIT_ERROR_REPORT = 'update-step-update-submit-error-report'; const UPDATE_STEP_UPDATE_DOWNLOAD_LOGS = 'update-step-update-download-logs'; /* step: post update */ diff --git a/classes/Traits/displayErrorReportDialogTrait.php b/classes/Traits/displayErrorReportDialogTrait.php new file mode 100644 index 000000000..9a4452275 --- /dev/null +++ b/classes/Traits/displayErrorReportDialogTrait.php @@ -0,0 +1,52 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ + +namespace PrestaShop\Module\AutoUpgrade\Traits; + +use PrestaShop\Module\AutoUpgrade\AjaxResponseBuilder; +use PrestaShop\Module\AutoUpgrade\DocumentationLinks; +use PrestaShop\Module\AutoUpgrade\Twig\PageSelectors; +use Symfony\Component\HttpFoundation\JsonResponse; + +trait displayErrorReportDialogTrait +{ + public function submitErrorReport(): JsonResponse + { + return AjaxResponseBuilder::hydrationResponse( + PageSelectors::DIALOG_PARENT_ID, + $this->getTwig()->render( + '@ModuleAutoUpgrade/dialogs/dialog-error-report.html.twig', + [ + 'dialogSize' => 'sm', + 'title' => $this->upgradeContainer->getTranslator()->trans('Send error report?'), + 'data_transparency_link' => DocumentationLinks::PRESTASHOP_PROJECT_DATA_TRANSPARENCY_URL, + ] + ), + ['addScript' => 'send-error-report-dialog'] + ); + } +} diff --git a/controllers/admin/self-managed/UpdatePageUpdateController.php b/controllers/admin/self-managed/UpdatePageUpdateController.php index d3525a0e8..64cd84556 100644 --- a/controllers/admin/self-managed/UpdatePageUpdateController.php +++ b/controllers/admin/self-managed/UpdatePageUpdateController.php @@ -35,16 +35,13 @@ use PrestaShop\Module\AutoUpgrade\Twig\UpdateSteps; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; +use PrestaShop\Module\AutoUpgrade\Traits\displayErrorReportDialogTrait; class UpdatePageUpdateController extends AbstractPageWithStepController { - const CURRENT_STEP = UpdateSteps::STEP_UPDATE; + use displayErrorReportDialogTrait; - // TODO: for dev update page comment need to be removed after dev -// public function index(): RedirectResponse -// { -// return $this->redirectTo(Routes::UPDATE_PAGE_BACKUP); -// } + const CURRENT_STEP = UpdateSteps::STEP_UPDATE; protected function getPageTemplate(): string { @@ -98,6 +95,7 @@ protected function getParams(): array 'success_route' => Routes::UPDATE_STEP_POST_UPDATE, 'download_logs_route' => Routes::UPDATE_STEP_UPDATE_DOWNLOAD_LOGS, 'restore_route' => Routes::RESTORE_PAGE_BACKUP_SELECTION, + 'submit_error_report_route' => Routes::UPDATE_STEP_UPDATE_SUBMIT_ERROR_REPORT, 'initial_process_action' => TaskName::TASK_UPDATE_INITIALIZATION, 'backup_available' => !empty($backupFinder->getAvailableBackups()), 'download_logs_parent_id' => PageSelectors::DOWNLOAD_LOGS_PARENT_ID, diff --git a/views/templates/dialogs/dialog-error-report.html.twig b/views/templates/dialogs/dialog-error-report.html.twig index 5a69f6d1c..53ac6324e 100644 --- a/views/templates/dialogs/dialog-error-report.html.twig +++ b/views/templates/dialogs/dialog-error-report.html.twig @@ -6,10 +6,10 @@ {{ 'Help us improve the module by sending us this error report. You can add details in the description if you want to.'|trans({}) }}

- {% if dataPrivacyLink %} + {% if data_transparency_link %}

- - Learn more about your data and your rightslaunch + + {{ 'Learn more about your data and your rights'|trans({}) }}launch

{% endif %} @@ -17,43 +17,45 @@ {% endblock %} {% block dialog_extra_content_inner %} -
+
-
+ {% endblock %} {% block dialog_footer %} From a03a892725524f5bb1f560574a5d426afcace8ba Mon Sep 17 00:00:00 2001 From: Alexis Guyomar Date: Thu, 5 Dec 2024 12:17:31 +0100 Subject: [PATCH 06/18] fix: missing using variable --- _dev/src/ts/dialogs/StartUpdateDialog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_dev/src/ts/dialogs/StartUpdateDialog.ts b/_dev/src/ts/dialogs/StartUpdateDialog.ts index 80437baf2..bf52f03ba 100644 --- a/_dev/src/ts/dialogs/StartUpdateDialog.ts +++ b/_dev/src/ts/dialogs/StartUpdateDialog.ts @@ -20,7 +20,7 @@ export default class StartUpdateDialog implements DomLifecycle { } get #form(): HTMLFormElement { - const form = document.forms.namedItem('form-confirm-update'); + const form = document.forms.namedItem(this.formId); if (!form) { throw new Error('Form not found'); } From a72e5207193d5f3f0eaa4da6c8f2af2430c296a5 Mon Sep 17 00:00:00 2001 From: Alexis Guyomar Date: Thu, 5 Dec 2024 14:52:55 +0100 Subject: [PATCH 07/18] feat: add send error report dialog script --- _dev/src/ts/api/sentryApi.ts | 107 +++++++++---------- _dev/src/ts/components/LogsViewer.ts | 3 + _dev/src/ts/dialogs/SendErrorReportDialog.ts | 79 ++++++++++++++ _dev/src/ts/pages/UpdatePageUpdate.ts | 15 ++- _dev/src/ts/routing/ScriptHandler.ts | 10 +- _dev/src/ts/types/sentryApi.ts | 18 ++++ views/templates/steps/update.html.twig | 6 +- 7 files changed, 171 insertions(+), 67 deletions(-) create mode 100644 _dev/src/ts/dialogs/SendErrorReportDialog.ts create mode 100644 _dev/src/ts/types/sentryApi.ts diff --git a/_dev/src/ts/api/sentryApi.ts b/_dev/src/ts/api/sentryApi.ts index d59720ec4..70cd755ff 100644 --- a/_dev/src/ts/api/sentryApi.ts +++ b/_dev/src/ts/api/sentryApi.ts @@ -1,6 +1,9 @@ import * as Sentry from '@sentry/browser'; import { SeverityLevel } from '@sentry/browser'; import { maskSensitiveInfoInUrl } from '../utils/urlUtils'; +import { Feedback, Logs, LogsFields } from '../types/sentryApi'; + +const adminDir = window.AutoUpgradeVariables.admin_dir; Sentry.init({ dsn: 'https://eae192966a8d79509154c65c317a7e5d@o298402.ingest.us.sentry.io/4507254110552064', @@ -12,74 +15,54 @@ Sentry.init({ } if (event.request?.url) { - event.request.url = maskSensitiveInfoInUrl( - window.location.href, - window.AutoUpgradeVariables.admin_dir - ); + event.request.url = maskSensitiveInfoInUrl(window.location.href, adminDir); } return event; }, beforeBreadcrumb(breadcrumb) { - if (breadcrumb.data?.url) { - breadcrumb.data.url = maskSensitiveInfoInUrl( - breadcrumb.data.url, - window.AutoUpgradeVariables.admin_dir - ); - } - - if (breadcrumb.data?.from) { - breadcrumb.data.from = maskSensitiveInfoInUrl( - breadcrumb.data.from, - window.AutoUpgradeVariables.admin_dir - ); - } - - if (breadcrumb.data?.to) { - breadcrumb.data.to = maskSensitiveInfoInUrl( - breadcrumb.data.to, - window.AutoUpgradeVariables.admin_dir - ); - } + ['url', 'from', 'to'].forEach((key) => { + if (breadcrumb.data?.[key]) { + breadcrumb.data[key] = maskSensitiveInfoInUrl(breadcrumb.data[key], adminDir); + } + }); return breadcrumb; } }); +/** + * Sends enriched user feedback to Sentry with optional logs and metadata. + * This function attaches log files, captures a custom event, and optionally sends user feedback with an associated event ID. + * + * @param {string} message - The message to describe the feedback or error. + * @param {Logs} [logs={}] - An object containing optional logs, warnings, and errors to attach. + * @param {Feedback} [feedback={}] - An object containing optional user feedback fields such as email and comments. + * @param {SeverityLevel} [level='error'] - The severity level of the event (e.g., 'info', 'warning', 'error'). + */ export function sendUserFeedback( message: string, - logs: { logs?: string; warnings?: string; errors?: string } = {}, - feedback: { email?: string; comments?: string } = {}, + logs: Logs = {}, + feedback: Feedback = {}, level: SeverityLevel = 'error' ) { - if (logs.logs) { - Sentry.getCurrentScope().addAttachment({ - filename: 'logs.txt', - data: logs.logs, - contentType: 'text/plain' - }); - } + const attachments: { key: LogsFields; filename: string }[] = [ + { key: LogsFields.LOGS, filename: 'logs.txt' }, + { key: LogsFields.WARNINGS, filename: 'summary_warnings.txt' }, + { key: LogsFields.ERRORS, filename: 'summary_errors.txt' } + ]; - if (logs.warnings) { - Sentry.getCurrentScope().addAttachment({ - filename: 'summary_warnings.txt', - data: logs.warnings, - contentType: 'text/plain' - }); - } - - if (logs.errors) { - Sentry.getCurrentScope().addAttachment({ - filename: 'summary_errors.txt', - data: logs.errors, - contentType: 'text/plain' - }); - } + attachments.forEach(({ key, filename }) => { + if (logs[key]) { + Sentry.getCurrentScope().addAttachment({ + filename, + data: logs[key], + contentType: 'text/plain' + }); + } + }); - const maskedUrl = maskSensitiveInfoInUrl( - window.location.href, - window.AutoUpgradeVariables.admin_dir - ); + const maskedUrl = maskSensitiveInfoInUrl(window.location.href, adminDir); const eventId = Sentry.captureEvent({ message, @@ -90,12 +73,20 @@ export function sendUserFeedback( }); if (feedback.email || feedback.comments) { - Sentry.captureFeedback({ - associatedEventId: eventId, - email: feedback.email || '', - message: feedback.comments || '', - url: maskedUrl - }); + Sentry.captureFeedback( + { + associatedEventId: eventId, + email: feedback.email, + message: feedback.comments || '' + }, + { + captureContext: { + tags: { + url: maskedUrl + } + } + } + ); } Sentry.getCurrentScope().clearAttachments(); diff --git a/_dev/src/ts/components/LogsViewer.ts b/_dev/src/ts/components/LogsViewer.ts index 17d98ebf8..34dd05d64 100644 --- a/_dev/src/ts/components/LogsViewer.ts +++ b/_dev/src/ts/components/LogsViewer.ts @@ -162,7 +162,10 @@ export default class LogsViewer extends ComponentAbstract implements Destroyable */ #createSummary(severity: SeverityClasses, logs: string[]): HTMLDivElement { const summaryFragment = this.#templateSummary.content.cloneNode(true) as DocumentFragment; + const summary = summaryFragment.querySelector('.logs__summary') as HTMLDivElement; + summary.setAttribute('data-summary-severity', severity); + const summaryScroll = summaryFragment.querySelector('.logs__summary-scroll') as HTMLDivElement; const title = this.#getSummaryTitle(severity); diff --git a/_dev/src/ts/dialogs/SendErrorReportDialog.ts b/_dev/src/ts/dialogs/SendErrorReportDialog.ts new file mode 100644 index 000000000..59e596252 --- /dev/null +++ b/_dev/src/ts/dialogs/SendErrorReportDialog.ts @@ -0,0 +1,79 @@ +import DomLifecycle from '../types/DomLifecycle'; +import { sendUserFeedback } from '../api/sentryApi'; +import { Feedback, FeedbackFields, Logs } from '../types/sentryApi'; + +export default class SendErrorReportDialog implements DomLifecycle { + protected readonly formId = 'form-error-feedback'; + + public mount = (): void => { + this.#form.addEventListener('submit', this.#onSubmit); + }; + + public beforeDestroy = (): void => { + this.#form.removeEventListener('submit', this.#onSubmit); + }; + + get #form(): HTMLFormElement { + const form = document.forms.namedItem(this.formId); + if (!form) { + throw new Error('Form not found'); + } + + return form; + } + + #onSubmit = (event: SubmitEvent) => { + event.preventDefault(); + + const logsViewer = document.querySelector( + '[data-component="progress-tracker"] [data-component="logs-viewer"]' + ); + + const logs: Logs = {}; + + const logsContent = logsViewer?.querySelector( + '[data-slot-component="scroll"] [data-slot-component="list"]' + ); + if (!logsContent) { + throw new Error('Logs content to send not found'); + } + + const message = logsContent.lastChild?.textContent; + if (!message) { + throw new Error('Message to send not found'); + } + + if (!logsContent.textContent) { + throw new Error('Logs to send not found'); + } + logs.logs = logsContent.textContent; + + const summaryWarningText = logsViewer?.querySelector( + '[data-summary-severity="warning"]' + )?.textContent; + if (summaryWarningText) { + logs.warnings = summaryWarningText; + } + + const summaryErrorText = logsViewer?.querySelector( + '[data-summary-severity="error"]' + )?.textContent; + if (summaryErrorText) { + logs.errors = summaryErrorText; + } + + const feedback: Feedback = {}; + + const form = event.target as HTMLFormElement; + const formData = new FormData(form); + + Object.values(FeedbackFields).forEach((field) => { + const value = formData.get(field); + if (value && typeof value === 'string') { + feedback[field] = value; + } + }); + + sendUserFeedback(message, logs, feedback); + }; +} diff --git a/_dev/src/ts/pages/UpdatePageUpdate.ts b/_dev/src/ts/pages/UpdatePageUpdate.ts index 0dba59ab3..ac53a0b7c 100644 --- a/_dev/src/ts/pages/UpdatePageUpdate.ts +++ b/_dev/src/ts/pages/UpdatePageUpdate.ts @@ -9,6 +9,7 @@ export default class UpdatePageUpdate extends UpdatePage { #progressTracker: ProgressTracker = new ProgressTracker(this.#progressTrackerContainer); #restoreAlertForm: null | HTMLFormElement = null; #restoreButtonForm: null | HTMLFormElement = null; + #submitErrorReportForm: null | HTMLFormElement = null; constructor() { super(); @@ -32,8 +33,9 @@ export default class UpdatePageUpdate extends UpdatePage { public beforeDestroy = () => { this.#progressTracker.beforeDestroy(); - this.#restoreAlertForm?.removeEventListener('submit', this.#handleRestoreSubmit); - this.#restoreButtonForm?.removeEventListener('submit', this.#handleRestoreSubmit); + this.#restoreAlertForm?.removeEventListener('submit', this.#handleSubmit); + this.#restoreButtonForm?.removeEventListener('submit', this.#handleSubmit); + this.#submitErrorReportForm?.addEventListener('submit', this.#handleSubmit); }; get #progressTrackerContainer(): HTMLDivElement { @@ -77,7 +79,7 @@ export default class UpdatePageUpdate extends UpdatePage { alertContainer.classList.remove('hidden'); this.#restoreAlertForm = document.forms.namedItem('restore-alert'); - this.#restoreAlertForm?.addEventListener('submit', this.#handleRestoreSubmit); + this.#restoreAlertForm?.addEventListener('submit', this.#handleSubmit); }; #displayErrorButtons = () => { @@ -89,11 +91,14 @@ export default class UpdatePageUpdate extends UpdatePage { buttonsContainer.classList.remove('hidden'); + this.#submitErrorReportForm = document.forms.namedItem('submit-error-report'); + this.#submitErrorReportForm?.addEventListener('submit', this.#handleSubmit); + this.#restoreButtonForm = document.forms.namedItem('restore-button'); - this.#restoreButtonForm?.addEventListener('submit', this.#handleRestoreSubmit); + this.#restoreButtonForm?.addEventListener('submit', this.#handleSubmit); }; - #handleRestoreSubmit = async (event: SubmitEvent) => { + #handleSubmit = async (event: SubmitEvent) => { event.preventDefault(); const form = event.target as HTMLFormElement; diff --git a/_dev/src/ts/routing/ScriptHandler.ts b/_dev/src/ts/routing/ScriptHandler.ts index 7c4caaf66..a4f5a717b 100644 --- a/_dev/src/ts/routing/ScriptHandler.ts +++ b/_dev/src/ts/routing/ScriptHandler.ts @@ -4,10 +4,13 @@ import UpdatePageUpdateOptions from '../pages/UpdatePageUpdateOptions'; import UpdatePageBackup from '../pages/UpdatePageBackup'; import UpdatePageUpdate from '../pages/UpdatePageUpdate'; import UpdatePagePostUpdate from '../pages/UpdatePagePostUpdate'; + +import StartUpdateDialog from '../dialogs/StartUpdateDialog'; +import SendErrorReportDialog from '../dialogs/SendErrorReportDialog'; + import DomLifecycle from '../types/DomLifecycle'; import { RoutesMatching } from '../types/scriptHandlerTypes'; import { routeHandler } from '../autoUpgrade'; -import StartUpdateDialog from '../dialogs/StartUpdateDialog'; export default class ScriptHandler { #currentScript: DomLifecycle | undefined; @@ -25,7 +28,8 @@ export default class ScriptHandler { 'update-page-update': UpdatePageUpdate, 'update-page-post-update': UpdatePagePostUpdate, - 'start-update-dialog': StartUpdateDialog + 'start-update-dialog': StartUpdateDialog, + 'send-error-report-dialog': SendErrorReportDialog }; /** @@ -42,7 +46,7 @@ export default class ScriptHandler { /** * @public - * @param {string} routeName - The name of the route to load his associated script. + * @param {string} scriptID - The name of the route to load his associated script. * @returns void * @description Loads and mounts the page script associated with the specified route name. */ diff --git a/_dev/src/ts/types/sentryApi.ts b/_dev/src/ts/types/sentryApi.ts new file mode 100644 index 000000000..2283b84ea --- /dev/null +++ b/_dev/src/ts/types/sentryApi.ts @@ -0,0 +1,18 @@ +enum LogsFields { + 'LOGS' = 'logs', + 'WARNINGS' = 'warnings', + 'ERRORS' = 'errors' +} + +enum FeedbackFields { + 'EMAIL' = 'email', + 'COMMENTS' = 'comments' +} + +export { LogsFields, FeedbackFields }; + +type Logs = Partial>; + +type Feedback = Partial>; + +export type { Logs, Feedback }; diff --git a/views/templates/steps/update.html.twig b/views/templates/steps/update.html.twig index c48caf34a..c645ba605 100644 --- a/views/templates/steps/update.html.twig +++ b/views/templates/steps/update.html.twig @@ -27,7 +27,11 @@ {% block buttons %} {% endblock %} From 0901ef4160e235916735fa4380e320a2267326b7 Mon Sep 17 00:00:00 2001 From: Alexis Guyomar Date: Wed, 11 Dec 2024 07:20:39 +0100 Subject: [PATCH 17/18] fix: feedback from review --- _dev/src/ts/dialogs/SendErrorReportDialog.ts | 8 ++------ _dev/src/ts/routing/ScriptHandler.ts | 2 +- views/templates/dialogs/dialog-error-report.html.twig | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/_dev/src/ts/dialogs/SendErrorReportDialog.ts b/_dev/src/ts/dialogs/SendErrorReportDialog.ts index 59e596252..ebde6ded7 100644 --- a/_dev/src/ts/dialogs/SendErrorReportDialog.ts +++ b/_dev/src/ts/dialogs/SendErrorReportDialog.ts @@ -25,15 +25,11 @@ export default class SendErrorReportDialog implements DomLifecycle { #onSubmit = (event: SubmitEvent) => { event.preventDefault(); - const logsViewer = document.querySelector( - '[data-component="progress-tracker"] [data-component="logs-viewer"]' - ); + const logsViewer = document.querySelector('[data-component="logs-viewer"]'); const logs: Logs = {}; - const logsContent = logsViewer?.querySelector( - '[data-slot-component="scroll"] [data-slot-component="list"]' - ); + const logsContent = logsViewer?.querySelector('[data-slot-component="list"]'); if (!logsContent) { throw new Error('Logs content to send not found'); } diff --git a/_dev/src/ts/routing/ScriptHandler.ts b/_dev/src/ts/routing/ScriptHandler.ts index a4f5a717b..f93758b07 100644 --- a/_dev/src/ts/routing/ScriptHandler.ts +++ b/_dev/src/ts/routing/ScriptHandler.ts @@ -46,7 +46,7 @@ export default class ScriptHandler { /** * @public - * @param {string} scriptID - The name of the route to load his associated script. + * @param {string} scriptID - The ID of the route to load his associated script. * @returns void * @description Loads and mounts the page script associated with the specified route name. */ diff --git a/views/templates/dialogs/dialog-error-report.html.twig b/views/templates/dialogs/dialog-error-report.html.twig index ad1fa04c4..6dd6807fa 100644 --- a/views/templates/dialogs/dialog-error-report.html.twig +++ b/views/templates/dialogs/dialog-error-report.html.twig @@ -20,7 +20,7 @@