From 6f41d2f4d43f33ce1be1e0880d69122aac397377 Mon Sep 17 00:00:00 2001 From: allrude Date: Mon, 6 Jul 2026 20:55:33 +0200 Subject: [PATCH 1/4] Make PHP 8.4 / Magento 2.4.8+ compatible; fix beacons & CSP; add docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compatibility, correctness, security and cleanup pass on the fork, plus full user documentation. Public config paths (basicrum/general/*) are unchanged. Functional fixes: - Initialise Boomerang for real. The template built basicRumBoomerangConfig but never passed it to Boomerang, so beacon_url / cookie flags / plugin settings were inert. Now pushes ["init", config] through BOOMR_mq. - Reimplement the "send after onload" delay with the public API (autorun:false + delayed BOOMR.page_ready) instead of a custom plugin that was never registered in the Boomerang build. - Add dynamic CSP: Model/Csp/BeaconPolicyCollector adds the configured beacon origin to connect-src + img-src, registered in CompositePolicyCollector. Without this, beacons are blocked on strict-CSP 2.4.x stores. Compatibility: - composer.json: PHP ~8.3||~8.4 (was ^8.1|^8.2|^8.3, excluded 8.4); replace "*" wildcards with bounded ranges; add module-config/backend/csp deps. - module.xml: declare for Store/Config/Backend/Csp. - PageTypeDetector: inject Response\Http (interface lacked getStatusCode()), strict ===404, map cms_noroute_index, memoise. Cleanup / security: - Footer ViewModel: typed, null-safe getBeaconEndpoint(); path constant. - footer.phtml: render nothing when endpoint empty; standardise on $escaper->escapeJs; drop redundant view-model re-fetch; keep SecureHtmlRenderer. - Trim PageTypeDetectorInterface to getPageType(); add return types to admin blocks; move Logo markup to an admin template + CSS (no inline styles). - Centralise Boomerang version in Model\Boomerang; rename vendored file to version-agnostic boomerang.min.js (it is release build 815, "cutting-edge" plugin flavor — a plugin-set name, not an unstable channel). - Beacon Endpoint field: validate-url + CSP comment. - Remove non-functional consent group + ConsentMode source model. - Add LICENSE (MIT + BSD-3 notice for Boomerang); correct README. Docs: - docs/user-guide.html: self-contained setup guide (cards,
, ) covering install, endpoint/token, config, CSP, verification, FAQ and troubleshooting. - CHANGELOG.md and IMPROVEMENT_PLAN.md documenting what/why. Verified: php -l clean on PHP 8.4; composer validate clean; setup:upgrade succeeds; Magento-bootstrapped runtime check confirms DI resolution, ViewModel null-safety, and that the CSP collector emits the correct policies and is registered. Browser-level beacon/CSP check left for the operator. Co-Authored-By: Claude Opus 4.8 (1M context) --- Api/PageTypeDetectorInterface.php | 27 +- .../System/Config/BoomerangVersion.php | 10 +- Block/Adminhtml/System/Config/ConsentMode.php | 17 - Block/Adminhtml/System/Config/Logo.php | 32 +- CHANGELOG.md | 89 +++ IMPROVEMENT_PLAN.md | 57 ++ LICENSE | 60 ++ Model/Boomerang.php | 30 + Model/Csp/BeaconPolicyCollector.php | 75 +++ Model/PageTypeDetector.php | 93 +-- README.md | 37 +- ViewModel/Footer.php | 29 +- composer.json | 22 +- docs/user-guide.html | 584 ++++++++++++++++++ etc/adminhtml/system.xml | 18 +- etc/di.xml | 8 +- etc/module.xml | 9 +- .../layout/adminhtml_system_config_edit.xml | 7 + .../templates/system/config/logo.phtml | 12 + view/adminhtml/web/css/basicrum-config.css | 14 + view/frontend/templates/footer.phtml | 96 +-- ...0.cutting-edge.min.js => boomerang.min.js} | 0 22 files changed, 1101 insertions(+), 225 deletions(-) delete mode 100644 Block/Adminhtml/System/Config/ConsentMode.php create mode 100644 CHANGELOG.md create mode 100644 IMPROVEMENT_PLAN.md create mode 100644 LICENSE create mode 100644 Model/Boomerang.php create mode 100644 Model/Csp/BeaconPolicyCollector.php create mode 100644 docs/user-guide.html create mode 100644 view/adminhtml/layout/adminhtml_system_config_edit.xml create mode 100644 view/adminhtml/templates/system/config/logo.phtml create mode 100644 view/adminhtml/web/css/basicrum-config.css rename view/frontend/web/js/boomr/{boomerang-1.815.60.cutting-edge.min.js => boomerang.min.js} (100%) diff --git a/Api/PageTypeDetectorInterface.php b/Api/PageTypeDetectorInterface.php index 57037d7..a1a3cf9 100644 --- a/Api/PageTypeDetectorInterface.php +++ b/Api/PageTypeDetectorInterface.php @@ -4,35 +4,16 @@ namespace BasicRum\Analytics\Api; /** - * Interface for page type detection service + * Resolves a coarse-grained page type for the current storefront request. + * + * @api */ interface PageTypeDetectorInterface { /** - * Get the current page type + * Get the current page type (e.g. "home", "product", "checkout", "404_not_found"). * * @return string */ public function getPageType(): string; - - /** - * Check if current page is homepage - * - * @return bool - */ - public function isHomePage(): bool; - - /** - * Check if current page is a product page - * - * @return bool - */ - public function isProductPage(): bool; - - /** - * Check if current page is a checkout page - * - * @return bool - */ - public function isCheckoutPage(): bool; } diff --git a/Block/Adminhtml/System/Config/BoomerangVersion.php b/Block/Adminhtml/System/Config/BoomerangVersion.php index 3d0bcc6..49b07f8 100644 --- a/Block/Adminhtml/System/Config/BoomerangVersion.php +++ b/Block/Adminhtml/System/Config/BoomerangVersion.php @@ -3,14 +3,16 @@ namespace BasicRum\Analytics\Block\Adminhtml\System\Config; +use BasicRum\Analytics\Model\Boomerang; use Magento\Config\Block\System\Config\Form\Field; -use Magento\Backend\Block\Template\Context; use Magento\Framework\Data\Form\Element\AbstractElement; class BoomerangVersion extends Field { - protected function _getElementHtml(AbstractElement $element) + protected function _getElementHtml(AbstractElement $element): string { - return 'Boomerang JS v. 1.815.60 - cutting-edge - 30 KB (gzipped)'; + return $this->escapeHtml( + sprintf('Boomerang JS v.%s (continuity flavor) - %s', Boomerang::VERSION, Boomerang::SIZE_HINT) + ); } -} \ No newline at end of file +} diff --git a/Block/Adminhtml/System/Config/ConsentMode.php b/Block/Adminhtml/System/Config/ConsentMode.php deleted file mode 100644 index 81ea151..0000000 --- a/Block/Adminhtml/System/Config/ConsentMode.php +++ /dev/null @@ -1,17 +0,0 @@ - 'explicit', 'label' => __('Explicit Consent')], - ['value' => 'implicit', 'label' => __('Implicit Consent')], - ['value' => 'cookie', 'label' => __('Cookie Banner')], - ['value' => 'gdpr', 'label' => __('GDPR Banner')] - ]; - } -} \ No newline at end of file diff --git a/Block/Adminhtml/System/Config/Logo.php b/Block/Adminhtml/System/Config/Logo.php index 607b187..74dd14c 100644 --- a/Block/Adminhtml/System/Config/Logo.php +++ b/Block/Adminhtml/System/Config/Logo.php @@ -4,29 +4,33 @@ namespace BasicRum\Analytics\Block\Adminhtml\System\Config; use Magento\Config\Block\System\Config\Form\Field; -use Magento\Backend\Block\Template\Context; use Magento\Framework\Data\Form\Element\AbstractElement; class Logo extends Field { - public function __construct( - Context $context, - array $data = [] - ) { - parent::__construct($context, $data); + /** + * @var string + */ + protected $_template = 'BasicRum_Analytics::system/config/logo.phtml'; + + /** + * Render the field as a full-width banner (no label/scope columns). + */ + public function render(AbstractElement $element): string + { + return $this->_toHtml(); } - public function render(AbstractElement $element) + protected function _getElementHtml(AbstractElement $element): string { - $html = '
'; - $html .= 'BasicRum Logo'; - $html .= 'BasicRUM Analytics'; - $html .= '
'; - return $html; + return $this->_toHtml(); } - protected function _getElementHtml(AbstractElement $element) + /** + * Static URL of the BasicRUM logo asset. + */ + public function getLogoUrl(): string { - return $this->render($element); + return $this->getViewFileUrl('BasicRum_Analytics::images/basicrum-log.svg'); } } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7c5b002 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,89 @@ +# Changelog + +All notable changes to the BasicRum Analytics module are documented here. +This project adheres to [Semantic Versioning](https://semver.org/). + +## [0.1.0] + +A compatibility, correctness, security and cleanup release. Two of the changes fix real functional +bugs (beacons were not actually initialised; beacons were blocked under strict CSP). The public +configuration path (`basicrum/general/*`) is unchanged, so existing configuration keeps working. + +### Fixed (functional bugs) + +- **Beacons are now actually initialised.** The frontend template built a `basicRumBoomerangConfig` + object (beacon URL, cookie flags, ResourceTiming/Continuity settings) but **never passed it to + Boomerang**, so those settings were inert. The bootstrap now initialises Boomerang through its method + queue: `BOOMR_mq.push(["init", config])`. _Why:_ without an `init` call Boomerang never learns the + beacon URL, so beacons could not be delivered as configured. +- **Deferred send reimplemented with the public API.** The old code defined a custom + `BOOMR.plugins.WaitAfterOnload` object that was never registered in Boomerang's plugin list, so it did + nothing. The "send a fixed delay after the page loads" behaviour is now implemented with the supported + `autorun: false` config plus a delayed `BOOMR.page_ready()` call on window `load`. _Why:_ custom + plugins must be compiled into the Boomerang build to run; the public `page_ready()` call achieves the + same intent reliably. +- **Content Security Policy: beacon host is whitelisted automatically.** Added + `Model/Csp/BeaconPolicyCollector` (a `Magento\Csp\Api\PolicyCollectorInterface`) that reads the + configured Beacon Endpoint and adds its origin to the `connect-src` and `img-src` fetch policies. + _Why:_ Magento 2.4+ ships strict CSP. Boomerang delivers beacons via XHR / `navigator.sendBeacon` + (`connect-src`) and image beacons (`img-src`); on a CSP-enforcing store those requests were blocked. + Because the endpoint is admin-configurable it cannot be a static `csp_whitelist.xml` entry, so the + host is resolved at runtime. + +### Changed (compatibility) + +- **PHP 8.3 / 8.4 support.** `composer.json` PHP constraint changed from `^8.1|^8.2|^8.3` + (which *excluded* 8.4) to `~8.3.0 || ~8.4.0`. _Why:_ Magento 2.4.8 requires PHP 8.3 and 2.4.9 adds + 8.4; the old constraint blocked installation on supported platforms. +- **Real dependency constraints.** Replaced the `magento/framework: *` / `magento/module-store: *` + wildcards with bounded ranges, and added the modules actually used at runtime: + `magento/module-config`, `magento/module-backend`, `magento/module-csp`. +- **Module load order.** `etc/module.xml` now declares a `` for `Magento_Store`, + `Magento_Config`, `Magento_Backend` and `Magento_Csp`. _Why:_ the module relies on these at runtime; + declaring them guarantees correct load/upgrade order. +- **`PageTypeDetector` uses the correct response type.** It injected the generic + `App\ResponseInterface` and then called `getStatusCode()`, which that interface does not declare. It + now injects `App\Response\Http`, uses a strict `=== 404` comparison, and maps `cms_noroute_index` to + the 404 page type. The result is memoised. + +### Changed (code quality & security) + +- `ViewModel/Footer` exposes a typed, null-safe `getBeaconEndpoint(): string` (returns `''` when unset) + and the config path constant `XML_PATH_BEACON_ENDPOINT`, replacing the loosely-typed `getConfig()` + array that could leak `null` into the template. +- The frontend template renders nothing when no endpoint is configured, standardises on the injected + `$escaper->escapeJs()`, drops a redundant view-model re-fetch, and keeps the CSP-safe + `SecureHtmlRenderer` script rendering. +- `Api\PageTypeDetectorInterface` trimmed to the single method in use (`getPageType()`); the unused + `isHomePage()/isProductPage()/isCheckoutPage()` methods were removed. +- Admin config field blocks gained return types. The `Logo` field's markup moved out of PHP into + `view/adminhtml/templates/system/config/logo.phtml` + `view/adminhtml/web/css/basicrum-config.css` + (loaded via `adminhtml_system_config_edit.xml`), removing inline styles and a redundant constructor. +- The Boomerang version string is centralised in `Model\Boomerang` (single source of truth) instead of + being duplicated across the JS filename and the admin display block. +- Beacon Endpoint field now validates as a URL (`validate-url`) and documents the CSP behaviour in its + admin comment. +- Added a real `LICENSE` (MIT for the module) with a **BSD-3-Clause third-party notice** for the bundled + Boomerang library, and corrected the README (it previously claimed PHP 7.2 / Magento 2.3). + +### Removed + +- **Non-functional consent UI.** The `consent` configuration group and its `ConsentMode` source model + were removed. _Why:_ the fields were exposed in the admin but never read by any code, so they gave a + false impression that consent gating was in effect. (Consent gating can be reintroduced as a real + feature later.) + +### Notes + +- The vendored Boomerang file was renamed from `boomerang-1.815.60.cutting-edge.min.js` to the + version-agnostic `boomerang.min.js`. Investigation confirmed this is **release build 815 with the + "cutting-edge" plugin flavor** (which bundles the Continuity plugin) — "cutting-edge" is a + *plugin-set name*, not an unstable channel; npm `latest` (1.815.1) is the same build. The code was + kept as-is; only the filename and version bookkeeping changed. +- To upgrade the bundled library later, replace `view/frontend/web/js/boomr/boomerang.min.js` and update + `Model\Boomerang::VERSION`. No template change is required (the path is version-agnostic). + +## [0.0.2] — previous + +- Initial fork baseline: ViewModel-based rendering, PHP 8 constructor property promotion, removed + setup version, Boomerang build without debug logging. diff --git a/IMPROVEMENT_PLAN.md b/IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..ba96c20 --- /dev/null +++ b/IMPROVEMENT_PLAN.md @@ -0,0 +1,57 @@ +# BasicRum Analytics — Improvement Plan + +Cleanup + PHP 8.4 / Magento 2.4.8+ compatibility + beacon fix. Status of each item is tracked below. + +## Decisions +- Target **PHP 8.3 / 8.4**, **Magento 2.4.8+**. +- Fix the beacon integration (boomerang was never `init`-ed) and add dynamic CSP for the beacon host. +- Remove the dead, non-functional consent UI. +- Ship a stable Boomerang build (not the "cutting-edge" one) with proper attribution. + +## Checklist + +### 1. Packaging & metadata — DONE +- [x] `composer.json`: PHP `~8.3.0 || ~8.4.0`; pinned real version ranges; added + `magento/module-config`, `magento/module-backend`, `magento/module-csp`; dropped `*` wildcards, + stale `archive.exclude`, and the module-level `repositories` block. +- [x] `LICENSE` added (MIT for module code + BSD-3-Clause third-party notice for Boomerang). +- [x] `README.md` corrected (PHP 8.3+/Magento 2.4.8+, working LICENSE link, CSP note). +- [x] `etc/module.xml`: `` for `Magento_Store`, `Magento_Config`, `Magento_Backend`, `Magento_Csp`. + +### 2. PHP cleanup & correctness — DONE +- [x] `Model/PageTypeDetector.php`: inject `Response\Http`; map `cms_noroute_index`; `=== 404`; memoize. +- [x] `Api/PageTypeDetectorInterface.php`: trimmed to `getPageType()`. +- [x] `ViewModel/Footer.php`: typed/null-safe `getBeaconEndpoint()`; scaffold comment removed; `readonly` promotion. +- [x] Admin blocks: return types added; `Logo` HTML moved to `view/adminhtml/templates/system/config/logo.phtml` + + `css/basicrum-config.css` (via `adminhtml_system_config_edit.xml`); redundant ctor & unused imports removed. +- [x] PSR-12 fixed; `etc/di.xml` scaffold comment removed. (Note: `final` deliberately NOT added to + DI-bound classes so Magento can still generate interceptors/plugins for them.) + +### 3. Beacon integration — `view/frontend/templates/footer.phtml` — DONE +- [x] Boomerang is now initialized via `BOOMR_mq.push(["init", config])` (previously never init-ed → inert). +- [x] Deferred send implemented with `autorun: false` + a delayed `BOOMR.page_ready()` on window load + (public API; replaces the never-registered custom `WaitAfterOnload` plugin). +- [x] Renders nothing when the endpoint is blank; standardized on `$escaper->escapeJs`; redundant + view-model re-fetch removed. Kept CSP-safe `SecureHtmlRenderer`. + +### 4. Dynamic CSP — `Model/Csp/BeaconPolicyCollector.php` + `etc/di.xml` — DONE +- [x] Collector adds the beacon origin (scheme+host+port) to `connect-src` + `img-src`; registered in + `Magento\Csp\Model\CompositePolicyCollector`. Verified at runtime. + +### 5. Remove dead consent config — DONE +- [x] `consent` group removed from `etc/adminhtml/system.xml`; `ConsentMode.php` deleted. + +### 6. Boomerang asset — DONE (see note) +- [x] Renamed to version-agnostic `js/boomr/boomerang.min.js`; version centralized in `Model\Boomerang`. +- Note: investigation showed the file is **release build 815 with the "cutting-edge" *plugin flavor*** + (bundles Continuity), NOT an unstable nightly — "cutting-edge" is a plugin-set name. npm `latest` + (1.815.1) is the same build. Per that finding we kept build 815 (same code, needed plugins) rather than + rebuild. To change the bundle later, replace the file and update `Model\Boomerang::VERSION`. + +## Verification — DONE +- [x] `composer validate` clean; `php -l` clean on PHP 8.4; all XML validated via `setup:upgrade`. +- [x] `module:enable` + `setup:upgrade` + `cache:flush` succeeded. +- [x] Runtime (Magento-bootstrapped): interface preference resolves; ViewModel null-safe; CSP collector + emits correct policies and is registered in the composite collector. +- [ ] TODO (needs a browser + configured endpoint): confirm the beacon actually fires with `p_type` + + `p_gen=mage2` and that there are no CSP violations for the beacon host. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..42f9f62 --- /dev/null +++ b/LICENSE @@ -0,0 +1,60 @@ +MIT License + +Copyright (c) BasicRUM (Tsvetan Stoychev) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------- + +THIRD-PARTY NOTICES + +This package bundles Boomerang, a Real User Monitoring library, under +view/frontend/web/js/boomr/. Boomerang is distributed under the BSD 3-Clause +License and is NOT covered by the MIT license above. + + Copyright (c) 2011, Yahoo! Inc. All rights reserved. + Copyright (c) 2011-2012, Log-Normal, Inc. All rights reserved. + Copyright (c) 2012-2017, SOASTA, Inc. All rights reserved. + Copyright (c) 2017-2023, Akamai Technologies, Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + + Boomerang project: https://github.com/akamai/boomerang diff --git a/Model/Boomerang.php b/Model/Boomerang.php new file mode 100644 index 0000000..5069614 --- /dev/null +++ b/Model/Boomerang.php @@ -0,0 +1,30 @@ +... + * "60" is the "cutting-edge" plugin flavor (bundles the Continuity plugin) of release build 815. + */ + public const VERSION = '1.815.60'; + + /** + * Approximate transferred size, shown in the admin for information only. + */ + public const SIZE_HINT = '~30 KB (gzipped)'; + + /** + * Version-agnostic view path of the bundled, minified library. + */ + public const JS_VIEW_PATH = 'BasicRum_Analytics::js/boomr/boomerang.min.js'; +} diff --git a/Model/Csp/BeaconPolicyCollector.php b/Model/Csp/BeaconPolicyCollector.php new file mode 100644 index 0000000..8f01d3f --- /dev/null +++ b/Model/Csp/BeaconPolicyCollector.php @@ -0,0 +1,75 @@ + connect-src, image beacons => img-src) so beacons are not blocked + * when Content-Security-Policy is enforced. + * + * The endpoint is admin-configurable, so it cannot be expressed as a static csp_whitelist.xml entry; + * this collector reads the current value at request time instead. + */ +class BeaconPolicyCollector implements PolicyCollectorInterface +{ + /** + * Fetch directives that must allow the beacon origin. + */ + private const DIRECTIVES = ['connect-src', 'img-src']; + + public function __construct( + private readonly ScopeConfigInterface $scopeConfig + ) { + } + + /** + * @inheritDoc + */ + public function collect(array $defaultPolicies = []): array + { + $host = $this->getBeaconOrigin(); + if ($host === null) { + return $defaultPolicies; + } + + foreach (self::DIRECTIVES as $directive) { + $defaultPolicies[] = new FetchPolicy($directive, false, [$host]); + } + + return $defaultPolicies; + } + + /** + * Scheme + host (+ port) of the configured beacon endpoint, or null when unset/invalid. + */ + private function getBeaconOrigin(): ?string + { + $endpoint = (string) $this->scopeConfig->getValue( + Footer::XML_PATH_BEACON_ENDPOINT, + ScopeInterface::SCOPE_STORE + ); + + if ($endpoint === '') { + return null; + } + + $parts = parse_url($endpoint); + if ($parts === false || empty($parts['host'])) { + return null; + } + + $origin = isset($parts['scheme']) ? $parts['scheme'] . '://' . $parts['host'] : $parts['host']; + if (isset($parts['port'])) { + $origin .= ':' . $parts['port']; + } + + return $origin; + } +} diff --git a/Model/PageTypeDetector.php b/Model/PageTypeDetector.php index de706d3..bd7beec 100644 --- a/Model/PageTypeDetector.php +++ b/Model/PageTypeDetector.php @@ -5,82 +5,53 @@ use BasicRum\Analytics\Api\PageTypeDetectorInterface; use Magento\Framework\App\Request\Http as HttpRequest; -use Magento\Framework\App\ResponseInterface; +use Magento\Framework\App\Response\Http as HttpResponse; class PageTypeDetector implements PageTypeDetectorInterface { + /** + * Full action name => page type. + */ + private const PAGE_TYPE_MAP = [ + 'cms_index_index' => 'home', + 'cms_page_view' => 'cms_page', + 'cms_noroute_index' => '404_not_found', + 'catalog_product_view' => 'product', + 'catalog_category_view' => 'category', + 'checkout_index_index' => 'checkout', + 'checkout_cart_index' => 'cart', + 'customer_account_login' => 'customer_login', + 'customer_account_create' => 'customer_register', + 'customer_account_index' => 'customer_account', + 'sales_order_history' => 'order_history', + 'contact_index_index' => 'contact', + 'catalogsearch_result_index' => 'search_results', + ]; + + private ?string $pageType = null; + public function __construct( - private HttpRequest $request, - private ResponseInterface $response + private readonly HttpRequest $request, + private readonly HttpResponse $response ) { } /** - * Get the current page type - * - * @return string + * @inheritDoc */ public function getPageType(): string { - // Check for error pages first - if ($this->response->getStatusCode() == 404) { - return '404_not_found'; + if ($this->pageType !== null) { + return $this->pageType; } - // Get full action name - $fullActionName = $this->request->getFullActionName(); - - // Common page types based on full action name - $pageTypeMap = [ - 'cms_index_index' => 'home', - 'cms_page_view' => 'cms_page', - 'catalog_product_view' => 'product', - 'catalog_category_view' => 'category', - 'checkout_index_index' => 'checkout', - 'checkout_cart_index' => 'cart', - 'customer_account_login' => 'customer_login', - 'customer_account_create' => 'customer_register', - 'customer_account_index' => 'customer_account', - 'sales_order_history' => 'order_history', - 'contact_index_index' => 'contact', - 'catalogsearch_result_index' => 'search_results', - ]; - - if (isset($pageTypeMap[$fullActionName])) { - return $pageTypeMap[$fullActionName]; + // An explicit 404 status wins over the action-name mapping. + if ($this->response->getStatusCode() === 404) { + return $this->pageType = '404_not_found'; } - // Default fallback - return 'unmapped_' . $fullActionName; - } + $fullActionName = (string) $this->request->getFullActionName(); - /** - * Check if current page is homepage - * - * @return bool - */ - public function isHomePage(): bool - { - return $this->getPageType() === 'home'; - } - - /** - * Check if current page is a product page - * - * @return bool - */ - public function isProductPage(): bool - { - return $this->getPageType() === 'product'; - } - - /** - * Check if current page is a checkout page - * - * @return bool - */ - public function isCheckoutPage(): bool - { - return $this->getPageType() === 'checkout'; + return $this->pageType = self::PAGE_TYPE_MAP[$fullActionName] ?? 'unmapped_' . $fullActionName; } } diff --git a/README.md b/README.md index 7acd972..092d8c0 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,17 @@ BasicRum Analytics is a Magento 2 extension that helps you collect and analyze real user monitoring (RUM) data for your Magento store, providing insights into your website's performance from the user's perspective. +It injects the [Boomerang](https://github.com/akamai/boomerang) RUM library on the storefront, tags each beacon with the current page type (`home`, `product`, `category`, `checkout`, …) and `p_gen=mage2`, and sends performance beacons to a configurable BasicRUM collector endpoint. + ## Requirements -- Magento Open Source or Commerce version 2.3.x or higher -- PHP 7.2 or higher +- Magento Open Source or Commerce **2.4.8** or higher +- PHP **8.3** or **8.4** ## Installation ```sh -composer require basicrum/basicrum-analytics +composer require basicrum/basicrum-analytics bin/magento module:enable BasicRum_Analytics bin/magento setup:upgrade bin/magento cache:flush @@ -19,13 +21,25 @@ bin/magento cache:flush ## Configuration 1. Log in to your Magento Admin Panel -2. Navigate to **Stores > Configuration > BasicRum Analytics** +2. Navigate to **Stores > Configuration > BasicRum > BasicRum Analytics** 3. Configure the following options: - - **Enable Module**: Set to "Yes" to enable the extension - - **Beacon Endpoint**: Enter the URL where the data should be sent. This will be the endpoint where a BasicRUM beacon catcher is running. + - **Enable**: Set to "Yes" to enable the extension + - **Beacon Endpoint**: The URL where beacons are sent. This is the endpoint where a BasicRUM beacon catcher is running. 4. Click "Save Config" to apply the changes -5. Clear the cache by going to **System > Cache Management** and clicking "Flush Magento Cache" +5. Flush the cache (**System > Cache Management > Flush Magento Cache**) + +### Content Security Policy + +The module registers a dynamic CSP policy collector that automatically whitelists the configured +**Beacon Endpoint** host under `connect-src` and `img-src`. No manual `csp_whitelist.xml` editing is +required when you change the endpoint — the host is derived from the configured value at runtime. + +## Documentation + +- **[User Guide](docs/user-guide.html)** — a complete, self-contained setup guide (open in a browser): + installation, getting your beacon endpoint/token, configuration, CSP, verification, FAQ and troubleshooting. +- **[CHANGELOG](CHANGELOG.md)** — what changed and why in each release. ## Verification @@ -33,10 +47,11 @@ To verify that the extension is working properly: 1. Open your store in a web browser 2. Open the browser's developer tools (F12) -3. Check the Network tab for requests to the BasicRum collection endpoint -4. Visit your BasicRum dashboard to confirm that data is being collected - +3. Check the Network tab for beacon requests to the configured Beacon Endpoint (they carry `p_type` and `p_gen=mage2`) +4. Confirm there are no CSP violations in the Console for the beacon host +5. Visit your BasicRum dashboard to confirm that data is being collected ## License -This extension is released under the [MIT License](LICENSE). +This extension's own code is released under the [MIT License](LICENSE). The bundled Boomerang library is +distributed under the BSD 3-Clause License — see the third-party notice in [LICENSE](LICENSE). diff --git a/ViewModel/Footer.php b/ViewModel/Footer.php index 4dd6813..478c22e 100644 --- a/ViewModel/Footer.php +++ b/ViewModel/Footer.php @@ -1,4 +1,5 @@ - $this->scopeConfig->getValue( - 'basicrum/general/beacon_endpoint', - ScopeInterface::SCOPE_STORE - ) - ]; - - return $config; + return (string) $this->scopeConfig->getValue( + self::XML_PATH_BEACON_ENDPOINT, + ScopeInterface::SCOPE_STORE + ); } /** - * Get the current page type + * Coarse-grained page type for the current request. */ public function getPageType(): string { diff --git a/composer.json b/composer.json index 806d241..b3efb06 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "basicrum/basicrum-analytics", "description": "BasicRUM Analytics Magento 2 module", "type": "magento2-module", - "version": "0.0.2", + "version": "0.1.0", "authors": [ { "name": "Tsvetan Stoychev", @@ -10,21 +10,13 @@ } ], "require": { - "php": "^8.1|^8.2|^8.3", - "magento/framework": "*", - "magento/module-store": "*" + "php": "~8.3.0 || ~8.4.0", + "magento/framework": ">=103.0.8 <104", + "magento/module-store": ">=101.1.0 <102", + "magento/module-config": ">=101.2.0 <102", + "magento/module-backend": ">=102.0.0 <103", + "magento/module-csp": ">=100.4.0 <101" }, - "archive": { - "exclude": [ - ".github" - ] - }, - "repositories": [ - { - "type": "composer", - "url": "https://repo.magento.com/" - } - ], "autoload": { "files": [ "registration.php" diff --git a/docs/user-guide.html b/docs/user-guide.html new file mode 100644 index 0000000..fde54ab --- /dev/null +++ b/docs/user-guide.html @@ -0,0 +1,584 @@ + + + + + +BasicRum Analytics for Magento 2 — Setup Guide + + + + +
+
+ +
+

BasicRum Analytics

+

Real User Monitoring for Magento 2 — setup, configuration & troubleshooting guide.

+
+
+ Module v0.1.0 + Magento 2.4.8+ + PHP 8.3 / 8.4 + Boomerang 1.815 + CSP-ready +
+
+
+ + + +
+ + +
+

i What this module does

+

BasicRum Analytics measures the real performance your visitors experience and + sends it to a collector you control, so you can see how fast your store actually is in the field — + not just in a lab.

+
+

Real User Monitoring

+

Loads the open-source Boomerang library to capture load time, ResourceTiming and + Continuity (responsiveness) metrics from every visitor.

+
🏷️

Page-type tagging

+

Every beacon is tagged with the page type (home, product, + checkout…) and p_gen=mage2 so you can segment in your dashboard.

+
🛡️

CSP-safe by design

+

Uses Magento's secure inline-script renderer and automatically whitelists your beacon host, so it + works on stores with Content-Security-Policy enforced.

+
+
No personal data collection built in. + The module ships performance beacons only. It stores nothing in your database and adds no server-side + endpoints — data goes straight from the browser to the collector URL you configure.
+
+ + +
+

1 Requirements

+
+
+

On your store

+
    +
  • Magento Open Source or Commerce 2.4.8 or newer
  • +
  • PHP 8.3 or 8.4
  • +
  • Command-line access for composer and bin/magento
  • +
+
+
+

Somewhere to send data

+

A BasicRUM beacon endpoint — the URL of a running beacon + catcher/collector that receives the performance data.

+ +
+
+
+ + +
+

2 Installation

+

Install through Composer (recommended), then enable the module and clear caches.

+
    +
  1. +

    Require the package

    +
    composer require basicrum/basicrum-analytics
    +
  2. +
  3. +

    Enable the module

    +
    bin/magento module:enable BasicRum_Analytics
    +
  4. +
  5. +

    Run the upgrade & clear caches

    +
    bin/magento setup:upgrade
    +bin/magento cache:flush
    +

    On a production-mode store also run + bin/magento setup:di:compile and re-deploy static content.

    +
  6. +
+
+ + +
+

3 Get your Beacon Endpoint (your "token")

+

The Beacon Endpoint is the single most important setting. It is the address your visitors' + browsers send data to — think of it as your connection string.

+ +
+
+

Self-hosted collector

+

Run the open-source BasicRUM beacon catcher and copy the + URL it listens on. It usually looks like:

+
https://rum.your-domain.com/beacon
+
+
+

Hosted / SaaS collector

+

Your provider's dashboard gives you a ready-made URL. + If it includes a site key / token, keep it as part of the URL:

+
https://collector.example.com/beacon?token=YOUR_TOKEN
+
+
+ +
About the "token" + This module version authenticates by URL — there is no separate token field. If your + collector issues an API key or site token, include it directly in the endpoint URL exactly as your + collector documents it (commonly a ?token=… query parameter or an ID in the path). Whatever + you paste into Beacon Endpoint is what the browser will call.
+ +

+
+ + +
+

4 Configure in the Admin

+
    +
  1. Open the settings

    +

    Log in to the Magento Admin and go to + Stores → Configuration → BasicRum → BasicRum Analytics.

  2. +
  3. Enable the module

    +

    Set Enable to Yes. (Nothing is injected on the storefront until this is on.)

  4. +
  5. Paste your Beacon Endpoint

    +

    Put the full URL from step 3 into Beacon Endpoint. It must be a valid URL — the + field validates this on save.

    +
  6. +
  7. Save & flush

    +

    Click Save Config, then flush the cache from + System → Cache Management (or run bin/magento cache:flush).

  8. +
+
Per-store setup + Beacon Endpoint can be set per website/store view. Switch the scope selector at the top-left of + the configuration page before saving if you run multiple brands or regions.
+
+ + +
+

5 Content Security Policy

+

Magento 2.4+ can enforce a Content-Security-Policy that blocks requests to unknown hosts. + This module handles that for you.

+
+

When you save a Beacon Endpoint, the module automatically adds that host to the + connect-src and img-src CSP directives, so beacons are never blocked — even in + restrict (enforced) mode. You do not need to edit csp_whitelist.xml.

+ +
+
If you front the collector through a CDN or a different host + make sure the host you enter in Beacon Endpoint is the one the browser actually connects to. The + whitelist is derived from that exact value (scheme + host + port).
+
+ + +
+

6 Verify it works

+
    +
  1. Open your storefront

    Visit any page of your store in a normal browser tab.

  2. +
  3. Open developer tools

    Press F12 (or ++I) and + select the Network tab.

  4. +
  5. Look for the beacon

    +

    Filter by your collector host. Within a few seconds of the page settling you should see a request to + your Beacon Endpoint carrying p_type and p_gen=mage2.

    +
  6. +
  7. Check the console

    +

    The Console tab should show no CSP violation errors mentioning your beacon + host. If it does, see Troubleshooting below.

  8. +
+
+ + +
+

7 Settings reference

+
+ + + + + + + + + + +
SettingConfig pathDescription
Enablebasicrum/general/enabledMaster switch. When off, nothing is injected on the storefront.
Beacon Endpointbasicrum/general/beacon_endpointFull URL of your collector. Validated as a URL; its host is auto-whitelisted for CSP.
Boomerang JS Versiondisplay onlyShows the bundled Boomerang build. Informational; not editable.
+
+

Settings honour Magento config scope (Default / Website / Store View).

+
+ + +
+

? Frequently asked questions

+ +
+ Do I need an API key or token?SETUP +

No separate token field exists in this version. Authentication is by URL: if your + collector requires a token or site key, include it in the Beacon Endpoint URL (for + example as ?token=…) exactly as your collector documents. A self-hosted BasicRUM catcher + typically needs no token at all.

+
+ +
+ Does this collect personal data or set tracking cookies?PRIVACY +

The module sends performance beacons (timings, resource metrics). Boomerang + sets a first-party session cookie to correlate a session's beacons; it is configured here as + secure and SameSite=Strict. It does not capture form contents or personal + identifiers. Always confirm your own privacy/consent obligations for your jurisdiction and collector.

+
+ +
+ Will it slow down my store?PERFORMANCE +

Boomerang is loaded asynchronously after the page is interactive, and + the beacon is sent a short delay after the page finishes loading — so measuring does not compete + with rendering. The library is ~30 KB gzipped and served from your own static domain.

+
+ +
+ Does it work with Hyvä / a headless front end?COMPATIBILITY +

Yes for standard Luma and Hyvä storefronts — it injects via a layout block into + before.body.end and uses CSP-safe inline script rendering. A fully headless (PWA) front end + does not render Magento layout, so you would integrate Boomerang in that front end instead.

+
+ +
+ Can I use a different Beacon Endpoint per store view?SCOPE +

Yes. Change the scope selector at the top of the configuration page to a specific + Website or Store View before entering the value and saving.

+
+ +
+ How do I upgrade the bundled Boomerang library?MAINTENANCE +

Replace view/frontend/web/js/boomr/boomerang.min.js with your new build + and update the VERSION constant in Model/Boomerang.php. The template references a + version-agnostic path, so no template edit is needed. Clear caches and re-deploy static content.

+
+ +
+ What happened to the "Consent Settings" section?CHANGES +

It was removed in v0.1.0. Those fields were shown in the admin but never actually did + anything (no consent gating was applied), which was misleading. Real consent gating can be added as a + proper feature in a future release.

+
+
+ + +
+

! Troubleshooting

+

Work top to bottom — the most common causes are first.

+ +
+ No beacon appears in the Network tabFIX +
+

Check, in order:

+
    +
  1. Enable is set to Yes and config is saved.
  2. +
  3. Beacon Endpoint is a full, correct URL. If it is empty, the module renders nothing.
  4. +
  5. You flushed the cache after saving (bin/magento cache:flush).
  6. +
  7. You waited a few seconds — the beacon is sent after the page fully loads.
  8. +
  9. No ad-blocker / privacy extension is blocking the request (test in a private window).
  10. +
+
+
+ +
+ Console shows a "Content Security Policy" / "Refused to connect" errorFIX +
+

The host being blocked must match your Beacon Endpoint host. Confirm the endpoint + host is exactly the host the browser connects to (including any CDN in front of it), then re-save and + flush caches so the CSP header is regenerated. If you set a custom CSP elsewhere, ensure it does not + strip connect-src/img-src additions.

+ +
+
+ +
+ "Please enter a valid URL" when saving the endpointFIX +

The field requires a full URL including scheme, e.g. https://rum.example.com/beacon + — not a bare host like rum.example.com. Add https:// and save again.

+
+ +
+ Composer error about PHP or Magento versionFIX +

This release targets PHP 8.3/8.4 and Magento 2.4.8+. + On older stacks Composer will refuse to install. Upgrade your platform, or pin to an older module release + that matches your environment.

+
+ +
+ The module is enabled but the admin section is missingFIX +

Run bin/magento setup:upgrade and flush caches. If your admin user cannot + see it, grant the Stores → Settings → Configuration → BasicRum Analytics ACL resource to the + admin role under System → User Roles.

+
+ +
+ Beacons work locally but not in productionFIX +

Production mode caches DI and static content. After changing config or updating the + module run bin/magento setup:di:compile, re-deploy static content, and flush caches. Also + confirm your production CSP mode (report-only vs restrict) and that the beacon host resolves publicly.

+
+
+ +
+

Still stuck? Check the module CHANGELOG.md and README.md, or open an + issue on the project repository with your Magento version, PHP version, and the exact Console/Network error.

+ +
+ + + + +

Finding your Beacon Endpoint

+
+

The Beacon Endpoint is provided by whatever receives your RUM data. Pick the path that + matches your setup:

+

A · Self-hosted BasicRUM

+
    +
  1. Deploy the open-source BasicRUM beacon catcher (see the BasicRUM project docs).
  2. +
  3. Note the public URL and path it listens on, e.g. https://rum.your-domain.com/beacon.
  4. +
  5. Make sure it is reachable over HTTPS from the public internet.
  6. +
+

B · Hosted / SaaS collector

+
    +
  1. Open your provider's dashboard.
  2. +
  3. Find the "beacon URL", "collector URL" or "install snippet".
  4. +
  5. Copy the full URL. If a token/site key is shown, keep it in the URL + (e.g. …/beacon?token=abc123).
  6. +
+
Rule of thumb + Whatever URL you would paste into a plain <script>-based install snippet's + beacon_url is exactly what goes into this field.
+
+
+ + +

Example configuration

+
+

A typical filled-in Stores → Configuration → BasicRum → BasicRum Analytics screen:

+
+ + + + + + +
EnableYes
Beacon Endpointhttps://rum.your-domain.com/beacon
Boomerang JS VersionBoomerang JS v.1.815.60 (continuity flavor) — ~30 KB (gzipped)
+
+

Then Save Config and flush the cache.

+
+
+ + +

Content Security Policy, explained

+
+

CSP is a browser security feature. Your store sends a Content-Security-Policy + header listing which hosts the page is allowed to talk to. Anything not on the list is blocked — which would + include your beacon if the collector host weren't whitelisted.

+

This module contributes a policy at runtime based on your configured endpoint. Conceptually, saving + https://rum.example.com/beacon results in the header including:

+
Content-Security-Policy:
+  connect-src 'self' https://rum.example.com;
+  img-src     'self' https://rum.example.com;
+  ...
+

Because it is derived from your setting, changing the endpoint updates the + whitelist automatically — there is no static file to maintain.

+
+
+ + +

Spotting the beacon in DevTools

+
+

In the Network tab, type your collector host into the filter box. Look for a request whose + name is your beacon path. Select it and check the payload — you should see variables like:

+
p_type = product        (the page type)
+p_gen  = mage2          (added by this module)
+rt.*   = ...            (Boomerang timing metrics)
+restiming = ...         (ResourceTiming, if enabled)
+

A 200 (or 204) status from your collector means the beacon + was accepted. No request at all → see the first Troubleshooting item.

+
+
+ + + +
+
+ BasicRum Analytics for Magento 2 · v0.1.0 + Module code: MIT · Bundled Boomerang: BSD-3-Clause · See LICENSE +
+
+ + + + diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 5b3dec9..223c74e 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -20,28 +20,16 @@ Magento\Config\Model\Config\Source\Yesno - + + validate-url + Full URL of your BasicRUM beacon catcher. Its host is whitelisted for CSP automatically. BasicRum\Analytics\Block\Adminhtml\System\Config\BoomerangVersion - - - - - Magento\Config\Model\Config\Source\Yesno - - - - BasicRum\Analytics\Block\Adminhtml\System\Config\ConsentMode - - 1 - - - diff --git a/etc/di.xml b/etc/di.xml index 354eb16..48f0f16 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -1,5 +1,11 @@ - + + + + BasicRum\Analytics\Model\Csp\BeaconPolicyCollector + + + diff --git a/etc/module.xml b/etc/module.xml index e88396f..13e0c15 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,5 +1,12 @@ - + + + + + + + + diff --git a/view/adminhtml/layout/adminhtml_system_config_edit.xml b/view/adminhtml/layout/adminhtml_system_config_edit.xml new file mode 100644 index 0000000..3b6d798 --- /dev/null +++ b/view/adminhtml/layout/adminhtml_system_config_edit.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/view/adminhtml/templates/system/config/logo.phtml b/view/adminhtml/templates/system/config/logo.phtml new file mode 100644 index 0000000..f04df83 --- /dev/null +++ b/view/adminhtml/templates/system/config/logo.phtml @@ -0,0 +1,12 @@ + + diff --git a/view/adminhtml/web/css/basicrum-config.css b/view/adminhtml/web/css/basicrum-config.css new file mode 100644 index 0000000..88cbed9 --- /dev/null +++ b/view/adminhtml/web/css/basicrum-config.css @@ -0,0 +1,14 @@ +.basicrum-config-logo { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin: 20px 0; + font-size: 2rem; + font-weight: 600; +} + +.basicrum-config-logo img { + width: 35px; + height: 35px; +} diff --git a/view/frontend/templates/footer.phtml b/view/frontend/templates/footer.phtml index c98dc10..98d1f94 100644 --- a/view/frontend/templates/footer.phtml +++ b/view/frontend/templates/footer.phtml @@ -7,70 +7,72 @@ use Magento\Framework\View\Element\Template; use Magento\Framework\View\Helper\SecureHtmlRenderer; /** @var Template $block */ -/** @var Footer $viewModel */ -/** @var SecureHtmlRenderer $secureRenderer */ /** @var Escaper $escaper */ +/** @var SecureHtmlRenderer $secureRenderer */ +/** @var Footer $viewModel */ $viewModel = $block->getViewModel(); -$config = $viewModel->getConfig(); -$pageType = $viewModel->getPageType(); +$beaconEndpoint = $viewModel->getBeaconEndpoint(); -$scriptString = <<