├── Api └── ReceiverInterface.php ├── Block └── Adminhtml │ └── Edit │ ├── BackButton.php │ ├── DeleteAllButton.php │ ├── DeleteButton.php │ └── IgnoreButton.php ├── Controller └── Adminhtml │ └── JavaScriptErrorReporting │ ├── Delete.php │ ├── DeleteAll.php │ ├── Detail.php │ ├── Grid.php │ ├── IgnoreHash.php │ ├── MassDelete.php │ ├── ResetIgnore.php │ └── Statistics.php ├── Cron ├── PruneIgnoreList.php └── PruneOldJobs.php ├── Model ├── Event.php ├── Reciever.php └── ResourceModel │ ├── Event.php │ └── Event │ └── Collection.php ├── README.md ├── Scope └── Config.php ├── Screenshot.png ├── Ui ├── Component │ └── Listing │ │ └── Column │ │ └── EventActions.php └── DataProvider │ ├── EventDataProvider.php │ └── SummaryDataProvider.php ├── ViewModel ├── Charts.php └── DisabledScopes.php ├── composer.json ├── etc ├── acl.xml ├── adminhtml │ ├── menu.xml │ ├── routes.xml │ └── system.xml ├── config.xml ├── crontab.xml ├── db_schema.xml ├── db_schema_whitelist.json ├── di.xml ├── module.xml └── webapi.xml ├── registration.php └── view ├── adminhtml ├── layout │ ├── fredden_javascripterrorreporting_detail.xml │ ├── fredden_javascripterrorreporting_grid.xml │ └── fredden_javascripterrorreporting_statistics.xml ├── templates │ ├── alert_config_disabled.phtml │ ├── ignored_hashes.phtml │ └── statistics │ │ └── charts.phtml ├── ui_component │ ├── fredden_javascripterrorreporting_detail.xml │ ├── fredden_javascripterrorreporting_grid.xml │ └── fredden_javascripterrorreporting_summary.xml └── web │ └── chart.js └── base ├── layout └── default.xml ├── templates └── script.phtml └── web └── error-handler.js /Api/ReceiverInterface.php: -------------------------------------------------------------------------------- 1 | __('Back'), 19 | 'on_click' => sprintf("location.href = '%s';", $this->getBackUrl()), 20 | 'class' => 'back', 21 | 'sort_order' => 10, 22 | ]; 23 | } 24 | 25 | protected function getBackUrl() 26 | { 27 | return $this->urlBuilder->getUrl('*/*/grid'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Block/Adminhtml/Edit/DeleteAllButton.php: -------------------------------------------------------------------------------- 1 | getRequest()->getParam('id'); 26 | $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 27 | 28 | if ($id) { 29 | $title = ""; 30 | try { 31 | $event = $this->eventFactory->create(); 32 | $event->load($id); 33 | $event->delete(); 34 | 35 | $this->messageManager->addSuccessMessage(__('The event has been deleted.')); 36 | } catch (Exception $e) { 37 | $this->messageManager->addErrorMessage($e->getMessage()); 38 | return $resultRedirect->setPath('*/*/detail', ['event_id' => $id]); 39 | } 40 | } else { 41 | $this->messageManager->addErrorMessage(__('Unable to find that event to delete.')); 42 | } 43 | 44 | return $resultRedirect->setPath('*/*/grid'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Controller/Adminhtml/JavaScriptErrorReporting/DeleteAll.php: -------------------------------------------------------------------------------- 1 | getRequest()->getParam('id'); 28 | $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 29 | 30 | if ($id) { 31 | $title = ""; 32 | try { 33 | $event = $this->eventFactory->create(); 34 | $event->load($id); 35 | 36 | $collection = $this->collectionFactory->create(); 37 | $collection->addFieldToFilter('hash', $event->getHash()); 38 | $collectionSize = $collection->getSize(); 39 | 40 | foreach ($collection as $event) { 41 | $event->delete(); 42 | } 43 | 44 | $this->messageManager->addSuccessMessage(__('Successfully deleted %1 event(s).', $collectionSize)); 45 | } catch (Exception $e) { 46 | $this->messageManager->addErrorMessage($e->getMessage()); 47 | return $resultRedirect->setPath('*/*/detail', ['event_id' => $id]); 48 | } 49 | } else { 50 | $this->messageManager->addErrorMessage(__('Unable to find that event to delete.')); 51 | } 52 | 53 | return $resultRedirect->setPath('*/*/grid'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Controller/Adminhtml/JavaScriptErrorReporting/Detail.php: -------------------------------------------------------------------------------- 1 | resultPageFactory->create(); 24 | $resultPage->setActiveMenu('Fredden_JavaScriptErrorReporting::view'); 25 | $resultPage->getConfig()->getTitle()->prepend(__('JavaScript Error Reporting')); 26 | return $resultPage; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Controller/Adminhtml/JavaScriptErrorReporting/Grid.php: -------------------------------------------------------------------------------- 1 | resultPageFactory->create(); 24 | $resultPage->setActiveMenu('Fredden_JavaScriptErrorReporting::view'); 25 | $resultPage->getConfig()->getTitle()->prepend(__('JavaScript Error Reporting')); 26 | return $resultPage; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Controller/Adminhtml/JavaScriptErrorReporting/IgnoreHash.php: -------------------------------------------------------------------------------- 1 | getRequest()->getParam('id'); 28 | $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 29 | 30 | if ($id) { 31 | try { 32 | $event = $this->eventFactory->create(); 33 | $event->load($id); 34 | 35 | $this->config->addHashToIgnore($event->getHash()); 36 | 37 | $this->messageManager->addSuccessMessage(__('The event has been hidden from the summary table.')); 38 | } catch (Exception $e) { 39 | $this->messageManager->addErrorMessage($e->getMessage()); 40 | } 41 | } else { 42 | $this->messageManager->addErrorMessage(__('Unable to find that event to hide.')); 43 | } 44 | 45 | return $resultRedirect->setPath('*/*/statistics'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Controller/Adminhtml/JavaScriptErrorReporting/MassDelete.php: -------------------------------------------------------------------------------- 1 | filter->getCollection($this->collectionFactory->create()); 27 | $collectionSize = $collection->getSize(); 28 | 29 | foreach ($collection as $event) { 30 | $event->delete(); 31 | } 32 | 33 | $this->messageManager->addSuccessMessage(__('Successfully deleted %1 event(s).', $collectionSize)); 34 | 35 | $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 36 | return $resultRedirect->setPath('*/*/grid'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Controller/Adminhtml/JavaScriptErrorReporting/ResetIgnore.php: -------------------------------------------------------------------------------- 1 | config->resetHashIgnoreList(); 25 | $this->messageManager->addSuccessMessage(__('The ignore list has been reset.')); 26 | 27 | $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); 28 | 29 | return $resultRedirect->setPath('*/*/statistics'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Controller/Adminhtml/JavaScriptErrorReporting/Statistics.php: -------------------------------------------------------------------------------- 1 | resultPageFactory->create(); 24 | $resultPage->setActiveMenu('Fredden_JavaScriptErrorReporting::view'); 25 | $resultPage->getConfig()->getTitle()->prepend(__('JavaScript Error Reporting')); 26 | return $resultPage; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Cron/PruneIgnoreList.php: -------------------------------------------------------------------------------- 1 | config->getIgnoredHashes(); 20 | 21 | if (!count($ignoredHashes)) { 22 | return; 23 | } 24 | 25 | $collection = $this->collectionFactory->create(); 26 | $collection->getSelect()->where('hash IN (?)', $ignoredHashes); 27 | $collection->getSelect()->group('hash'); 28 | 29 | $newHashes = []; 30 | foreach ($collection as $event) { 31 | $newHashes[] = $event->getHash(); 32 | } 33 | 34 | $this->config->replaceIgnoreList(implode(',', $newHashes)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Cron/PruneOldJobs.php: -------------------------------------------------------------------------------- 1 | config->getDaysToKeep(); 26 | if ($daysToKeep <= 0) { 27 | return; 28 | } 29 | 30 | $date = new DateTime(); 31 | $date->sub(new DateInterval('P' . $daysToKeep . 'D')); 32 | 33 | $collection = $this->collectionFactory->create() 34 | ->setPageSize(self::EVENTS_PER_RUN) 35 | ->addFieldToFilter('created_at', [ 36 | 'lteq' => $date->format('Y-m-d H:i:s') 37 | ]); 38 | 39 | if ($collection->getSize() > self::EVENTS_PER_RUN) { 40 | // There are more events to delete than we'll process now; let's run 41 | // this job again. We don't want to loop over too many events to 42 | // avoid out-of-memory issues. We are scheduling this before the 43 | // delete loop below in case the latter runs out of memory anyway. 44 | $this->scheduleFactory->create() 45 | ->setJobCode('fredden_javascript_error_reporting_prune') 46 | ->setStatus(Schedule::STATUS_PENDING) 47 | ->setCreatedAt(date('Y-m-d H:i:s')) 48 | ->setScheduledAt(date('Y-m-d H:i:s')) 49 | ->save(); 50 | } 51 | 52 | foreach ($collection as $event) { 53 | $event->delete(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Model/Event.php: -------------------------------------------------------------------------------- 1 | _init(ResourceModel\Event::class); 12 | } 13 | 14 | public function beforeSave() 15 | { 16 | $fileWithoutVersion = preg_replace( 17 | '_/static/version\d+/_', 18 | '/static/', 19 | $this->getErrorFile() 20 | ); 21 | 22 | $this->setHash(md5(implode('|', [ 23 | $this->getErrorMessage(), 24 | $fileWithoutVersion, 25 | $this->getLine(), 26 | $this->getColumn(), 27 | ]))); 28 | 29 | return parent::beforeSave(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Model/Reciever.php: -------------------------------------------------------------------------------- 1 | request->getBodyParams(); 23 | if (!isset($parameters['browser']) || !isset($parameters['event'])) { 24 | return; 25 | } 26 | 27 | $event = $this->eventFactory->create(); 28 | 29 | $event->setUserAgent($this->request->getHeader('user-agent', null)); 30 | $event->setReferrer($this->request->getHeader('referer', null)); 31 | 32 | $event->setBrowserHeight($parameters['browser']['height'] ?? 0); 33 | $event->setBrowserWidth($parameters['browser']['width'] ?? 0); 34 | $event->setUrl($parameters['browser']['url'] ?? null); 35 | 36 | $event->setErrorMessage($parameters['event']['message'] ?? ''); 37 | $event->setStackTrace($parameters['event']['stack'] ?? ''); 38 | $event->setErrorFile($parameters['event']['filename'] ?? ''); 39 | $event->setLine($parameters['event']['lineno'] ?? 0); 40 | $event->setColumn($parameters['event']['colno'] ?? 0); 41 | $event->setTimer($parameters['event']['timer'] ?? 0); 42 | 43 | $event->save(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Model/ResourceModel/Event.php: -------------------------------------------------------------------------------- 1 | _init('fredden_javascript_error_report', 'event_id'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Model/ResourceModel/Event/Collection.php: -------------------------------------------------------------------------------- 1 | _init(Model::class, ResourceModel::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript error reporting for Magento 2 2 | 3 | [![Latest stable version](https://img.shields.io/packagist/v/fredden/magento2-module-javascript-error-reporting?style=plastic)](https://packagist.org/packages/fredden/magento2-module-javascript-error-reporting) 4 | [![Total downloads](https://img.shields.io/packagist/dt/fredden/magento2-module-javascript-error-reporting?style=plastic)](https://packagist.org/packages/fredden/magento2-module-javascript-error-reporting/stats) 5 | [![Code quality alerts](https://img.shields.io/lgtm/alerts/g/fredden/magento2-module-javascript-error-reporting.svg?logo=lgtm&style=plastic)](https://lgtm.com/projects/g/fredden/magento2-module-javascript-error-reporting/alerts/) 6 | 7 | ## Overview 8 | A Magento 2 module which captures JavaScript errors for later review by website administrators. 9 | JavaScript errors are kept for up to 180 days (configurable) and available via Magento's administration back-end. 10 | 11 |
Screen-shot of summary view in admin area 12 | 13 | ![](Screenshot.png) 14 | 15 |
16 | 17 | ## Features 18 | * JavaScript errors are recorded for later review 19 | - JavaScript error message 20 | - File (URL), line number, column number of exception if available 21 | - Stack trace of exception if available 22 | * Limited browser information which might be useful for debugging (but hopefully not for fingerprinting / identifying individual users) is recorded 23 | - Current URL when error occurred 24 | - User agent 25 | - Viewport width & height 26 | * Errors are kept for up to 180 days (configurable) and automatically deleted after this period 27 | * Errors are available for review in Magento's back-end 28 | - Admin -> Reports -> JavaScript Error Reporting 29 | * Errors can be marked as ignored to help reduce noise / focus on new errors 30 | * Module can be enabled / disabled via configuration 31 | - Admin -> Stores -> Settings -> Configuration -> Advanced -> System -> JavaScript Error Reporting 32 | 33 | ## Installation 34 | This module is available on packagist.org and installable via composer: 35 | 36 | ```sh 37 | composer require fredden/magento2-module-javascript-error-reporting 38 | ``` 39 | 40 | This module uses [semantic versioning (semver)](http://semver.org/). 41 | 42 | ## Compatibility 43 | |Version|Magento Open Source|Adobe Commerce| 44 | |-|-|-| 45 | |2.0.x|:x: *unsupported*|:x: *unsupported*| 46 | |2.1.x|:x: *unsupported*|:x: *unsupported*| 47 | |2.2.x|:white_check_mark: Yes `^0.1`|:white_check_mark: Yes `^0.1`| 48 | |2.3.x|:white_check_mark: Yes `^1.2.2`|:white_check_mark: Yes `^1.2.2`| 49 | |2.4.x|:white_check_mark: Yes `^1.0.1`|:white_check_mark: Yes `^1.0.1`| 50 | |2.4.4|:white_check_mark: Yes `^1.0.1 \|\| ^2.0`|:white_check_mark: Yes `^1.0.1 \|\| ^2.0`| 51 | |2.4.5|:white_check_mark: Yes `^1.0.1 \|\| ^2.0`|:white_check_mark: Yes `^1.0.1 \|\| ^2.0`| 52 | |2.4.6|:white_check_mark: Yes `^1.0.1 \|\| ^2.0`|:white_check_mark: Yes `^1.0.1 \|\| ^2.0`| 53 | 54 | PHP version 7.1 or better is required. 55 | 56 | No third-party libraries nor services are required. 57 | 58 | ## Contributing 59 | Community contributions are welcome. 60 | Please open a pull request on GitHub if you have a code suggestion, 61 | or an issue on GitHub if you are encountering a problem with this module. 62 | Please note that issues relating to the problems that this module highlights in others' code are out of scope for support here. 63 | 64 | ## Future development ideas 65 | 66 | - filter grid by date **and time** range 67 | - details of "errors like this" (same hash) on details page. 68 | - perhaps links for: first, last, next, previous 69 | - maybe a chart of this error over time 70 | - show same charts as statistics page, but specifically for this error 71 | - statistics page, link for details should go to latest not first instance of error 72 | - charts on statistics page, click node to go to details pane with this time filter applied 73 | -------------------------------------------------------------------------------- /Scope/Config.php: -------------------------------------------------------------------------------- 1 | scopeConfig->getValue(self::XML_PATH_DAYS_TO_KEEP); 26 | } 27 | 28 | public function getIgnoredHashes(): array 29 | { 30 | $data = (string) $this->flagManager->getFlagData(self::FLAG_NAME); 31 | 32 | if ($data) { 33 | return explode(',', $data); 34 | } 35 | 36 | return []; 37 | } 38 | 39 | public function addHashToIgnore(string $hashToAdd): void 40 | { 41 | $data = (string) $this->flagManager->getFlagData(self::FLAG_NAME); 42 | 43 | if ($data) { 44 | $hashToAdd .= ','; 45 | } 46 | 47 | $this->flagManager->saveFlag(self::FLAG_NAME, $hashToAdd . $data); 48 | } 49 | 50 | public function removeHashFromIgnore(string $hashToRemove): void 51 | { 52 | $data = (string) $this->flagManager->getFlagData(self::FLAG_NAME); 53 | $data = str_replace(",$hashToRemove,", ',', ",$data,"); 54 | $data = trim($data, ','); 55 | $this->flagManager->saveFlag(self::FLAG_NAME, $data); 56 | } 57 | 58 | public function resetHashIgnoreList(): void 59 | { 60 | $this->flagManager->deleteFlag(self::FLAG_NAME); 61 | } 62 | 63 | public function replaceIgnoreList(string $newList): void 64 | { 65 | $this->flagManager->saveFlag(self::FLAG_NAME, $newList); 66 | } 67 | 68 | public function getChartDisplayHowManyHours(): int 69 | { 70 | $value = (int) $this->scopeConfig->getValue(self::XML_PATH_CHART_SHOW_HOURS); 71 | $keep = $this->getDaysToKeep(); 72 | 73 | return $keep ? min($value, $keep * 24) : $value; 74 | } 75 | 76 | public function getChartDisplayHowManyDays(): int 77 | { 78 | $value = (int) $this->scopeConfig->getValue(self::XML_PATH_CHART_SHOW_DAYS); 79 | $keep = $this->getDaysToKeep(); 80 | 81 | return $keep ? min($value, $keep) : $value; 82 | } 83 | 84 | public function getChartDisplayHowManyWeeks(): int 85 | { 86 | $value = (int) $this->scopeConfig->getValue(self::XML_PATH_CHART_SHOW_WEEKS); 87 | $keep = $this->getDaysToKeep(); 88 | 89 | return $keep ? min($value, ceil($keep / 7)) : $value; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fredden/magento2-module-javascript-error-reporting/7376b7bc4e1dbc78c673aa31b11876d44ac36c48/Screenshot.png -------------------------------------------------------------------------------- /Ui/Component/Listing/Column/EventActions.php: -------------------------------------------------------------------------------- 1 | getData('name')] = [ 14 | 'view' => [ 15 | 'label' => __('View details'), 16 | 'href' => $this->context->getUrl( 17 | 'fredden/JavaScriptErrorReporting/detail', 18 | ['id' => $item[$this->getConfig()['indexField']]] 19 | ), 20 | ], 21 | ]; 22 | } 23 | } 24 | 25 | return $dataSource; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Ui/DataProvider/EventDataProvider.php: -------------------------------------------------------------------------------- 1 | collection = $collectionFactory->create(); 22 | 23 | $ignoredHashes = $config->getIgnoredHashes(); 24 | if ($ignoredHashes) { 25 | $this->collection->getSelect()->where('hash NOT IN (?)', $ignoredHashes); 26 | } 27 | } 28 | 29 | public function getData() 30 | { 31 | $data = parent::getData(); 32 | 33 | if (!empty($data['items'])) { 34 | foreach ($data['items'] as &$item) { 35 | foreach (['error_file', 'referrer', 'url'] as $key) { 36 | $item[$key] = str_replace( 37 | ';', 38 | ';', 39 | htmlentities( 40 | $item[$key], 41 | ENT_QUOTES | ENT_HTML5 42 | ) 43 | ); 44 | 45 | // Split/wrap hashes in URLs, eg /admin/dashboard/index/key/{hash}/ 46 | $item[$key] = preg_replace( 47 | '_/([a-f0-9]{16})([a-f0-9]{16})([a-f0-9]{16})([a-f0-9]{16})/_', 48 | '/\1\2\3\4/', 49 | $item[$key] 50 | ); 51 | } 52 | } 53 | 54 | unset($item); // avoid side effects from $item being a reference 55 | } 56 | 57 | return $data; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Ui/DataProvider/SummaryDataProvider.php: -------------------------------------------------------------------------------- 1 | collection->addExpressionFieldToSelect('count', 'COUNT(event_id)', ''); 23 | $this->collection->addExpressionFieldToSelect('first', 'MIN(created_at)', ''); 24 | $this->collection->addExpressionFieldToSelect('last', 'MAX(created_at)', ''); 25 | 26 | $this->collection->getSelect()->group('hash'); 27 | $this->collection->getSelect()->order('count ' . Select::SQL_DESC); 28 | $this->collection->getSelect()->limit(100); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ViewModel/Charts.php: -------------------------------------------------------------------------------- 1 | hash = $value; 28 | $this->data = null; 29 | } 30 | 31 | public function getData(): array 32 | { 33 | if (isset($this->data)) { 34 | return $this->data; 35 | } 36 | 37 | // We are using the Select object directly here because using a collection is unreasonably slow 38 | $select = $this->collectionFactory->create()->getSelect(); 39 | $select->reset(Select::COLUMNS); 40 | $select->columns(['timestamp' => 'UNIX_TIMESTAMP(created_at)']); 41 | $select->order('created_at DESC'); 42 | 43 | if (isset($this->hash)) { 44 | $select->where('hash = ?', $this->hash); 45 | } elseif ($ignoreHashes = $this->config->getIgnoredHashes()) { 46 | $select->where('hash NOT IN (?)', $ignoreHashes); 47 | } 48 | 49 | $dbResult = $select->getConnection()->fetchAll($select, [], \Zend_Db::FETCH_COLUMN); 50 | 51 | $this->data = []; 52 | foreach ($dbResult as $item) { 53 | $this->data[] = (int) $item; 54 | } 55 | 56 | return $this->data; 57 | } 58 | 59 | public function getHourlyData(): string 60 | { 61 | return $this->getJsonData( 62 | 3600, 63 | $this->config->getChartDisplayHowManyHours(), 64 | 'jS M H:i' 65 | ); 66 | } 67 | 68 | public function getDailyData(): string 69 | { 70 | return $this->getJsonData( 71 | 86400, 72 | $this->config->getChartDisplayHowManyDays(), 73 | 'D jS M' 74 | ); 75 | } 76 | 77 | public function getWeeklyData(): string 78 | { 79 | return $this->getJsonData( 80 | 604800, 81 | $this->config->getChartDisplayHowManyWeeks(), 82 | 'Y-M-d' 83 | ); 84 | } 85 | 86 | private function getJsonData(int $period, int $dataPointCount, string $dateFormat): string 87 | { 88 | $threshold = time() - ($period * $dataPointCount); 89 | 90 | $dataPoints = []; 91 | for ($i = time(); $i > $threshold; $i -= $period) { 92 | $key = floor($i / $period); 93 | $dataPoints[$key] = [ 94 | 'start' => $i - ($period + 1), 95 | 'end' => $i, 96 | 'count' => 0, 97 | ]; 98 | } 99 | ksort($dataPoints); 100 | 101 | foreach ($this->getData() as $event) { 102 | $key = floor($event / $period); 103 | if (!isset($dataPoints[$key])) { 104 | break; 105 | } 106 | $dataPoints[$key]['count']++; 107 | } 108 | 109 | $labels = []; 110 | $values = []; 111 | foreach ($dataPoints as $point) { 112 | $start = $this->timezone->date($point['start'])->format($dateFormat); 113 | $end = $this->timezone->date($point['end'])->format($dateFormat); 114 | 115 | $labels[] = "$start - $end"; 116 | $values[] = $point['count']; 117 | } 118 | 119 | return $this->json->serialize([ 120 | 'labels' => $labels, 121 | 'values' => $values, 122 | ]); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /ViewModel/DisabledScopes.php: -------------------------------------------------------------------------------- 1 | configCollectionFactory->create(); 18 | $collection->addFieldToFilter('path', ['eq' => 'system/fredden_javascript_error_reporting/enabled']); 19 | $collection->addFieldToFilter('value', ['eq' => '0']); 20 | 21 | $result = []; 22 | 23 | foreach ($collection as $configValue) { 24 | /** @var \Magento\Framework\App\Config\Value $configValue */ 25 | $scope = rtrim($configValue->getScope(), 's'); 26 | $scopeId = $configValue->getScopeId(); 27 | 28 | $result[] = [$scope, $scopeId]; 29 | } 30 | 31 | return $result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fredden/magento2-module-javascript-error-reporting", 3 | "description": "A Magento 2 module which captures JavaScript errors for later review by website administrators", 4 | "license": "CC-BY-NC-SA-4.0", 5 | "type": "magento2-module", 6 | "authors": [ 7 | { 8 | "name": "Dan Wallis", 9 | "email": "dan@wallis.nz" 10 | } 11 | ], 12 | "homepage": "https://github.com/fredden/magento2-module-javascript-error-reporting", 13 | "require": { 14 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0", 15 | "ext-pcre": "*", 16 | "magento/framework": "^103.0.4", 17 | "magento/module-backend": "^102.0.4", 18 | "magento/module-config": "^101.2.4", 19 | "magento/module-cron": "^100.4.4", 20 | "magento/module-reports": "^100.4.4", 21 | "magento/module-store": "^101.1.4", 22 | "magento/module-ui": "^101.2.4" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Fredden\\JavaScriptErrorReporting\\": "" 27 | }, 28 | "files": [ 29 | "registration.php" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /etc/adminhtml/menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /etc/adminhtml/routes.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 |
6 | 12 | 13 | 21 | 22 | Magento\Config\Model\Config\Source\Yesno 23 | 24 | 32 | 33 | 0 (zero) to keep errors forever]]> 34 | integer no-whitespace required-entry validate-zero-or-greater 35 | 36 | 44 | 45 | integer no-whitespace required-entry validate-greater-than-zero 46 | 47 | 55 | 56 | integer no-whitespace required-entry validate-greater-than-zero 57 | 58 | 66 | 67 | integer no-whitespace required-entry validate-greater-than-zero 68 | 69 | 70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 1 8 | 180 9 | 48 10 | 21 11 | 52 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /etc/crontab.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 47 23 * * * 9 | 10 | 13 | 4 10 * * 0 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /etc/db_schema.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /etc/db_schema_whitelist.json: -------------------------------------------------------------------------------- 1 | { 2 | "fredden_javascript_error_report": { 3 | "column": { 4 | "event_id": true, 5 | "created_at": true, 6 | "user_agent": true, 7 | "referrer": true, 8 | "url": true, 9 | "browser_width": true, 10 | "browser_height": true, 11 | "error_message": true, 12 | "stack_trace": true, 13 | "error_file": true, 14 | "line": true, 15 | "column": true, 16 | "timer": true, 17 | "hash": true 18 | }, 19 | "constraint": { 20 | "PRIMARY": true 21 | }, 22 | "index": { 23 | "FREDDEN_JAVASCRIPT_ERROR_REPORT_CREATED_AT": true, 24 | "FREDDEN_JAVASCRIPT_ERROR_REPORT_HASH": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /etc/webapi.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /view/adminhtml/layout/fredden_javascripterrorreporting_grid.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Fredden\JavaScriptErrorReporting\ViewModel\DisabledScopes 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Fredden\JavaScriptErrorReporting\Scope\Config 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /view/adminhtml/layout/fredden_javascripterrorreporting_statistics.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Fredden\JavaScriptErrorReporting\ViewModel\DisabledScopes 9 | 10 | 11 | 12 | 13 | Fredden\JavaScriptErrorReporting\ViewModel\Charts 14 | Fredden\JavaScriptErrorReporting\Scope\Config 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Fredden\JavaScriptErrorReporting\Scope\Config 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /view/adminhtml/templates/alert_config_disabled.phtml: -------------------------------------------------------------------------------- 1 | getDisabledScopes()->getDisabledScopes(); 5 | 6 | if ($disabledScopes): ?> 7 | 33 | getConfig()->getIgnoredHashes()); 4 | 5 | if ($count): ?> 6 |

7 | escapeHtml(__('The above list has been filtered to ignore %1 errors.', $count)) ?> 8 |

9 | 10 |
11 | 12 | 13 | 16 |
17 | getConfig(); 6 | /** @var \Fredden\JavaScriptErrorReporting\ViewModel\Charts $charts */ 7 | $charts = $block->getCharts(); 8 | ?> 9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 | 43 | -------------------------------------------------------------------------------- /view/adminhtml/ui_component/fredden_javascripterrorreporting_detail.xml: -------------------------------------------------------------------------------- 1 | 2 |
4 | 5 | 6 | fredden_javascripterrorreporting_detail.fredden_javascripterrorreporting_data_source 7 | 8 | templates/form/collapsible 9 | 10 | 11 | 12 | 13 | 17 | 18 | fredden_javascripterrorreporting_grid_columns 19 | 20 | fredden_javascripterrorreporting_grid.fredden_javascripterrorreporting_data_source 21 | 22 |
23 | 24 | 25 | 26 | 27 | event_id 28 | 29 | 30 | 31 | 32 | 33 | event_id 34 | event_id 35 | 36 | 37 | 38 | 39 | 40 | 41 | true 42 | 43 | 44 | 45 | 46 | 47 | 48 | Are you sure you want to delete selected items? 49 | Delete items 50 | 51 | 52 | delete 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | event_id 64 | false 65 | 66 | 67 | 68 | 69 | 70 | textRange 71 | 72 | 73 | 74 | 75 | 76 | 77 | dateRange 78 | date 79 | desc 80 | 81 | 82 | 83 | 84 | 85 | 86 | text 87 | ui/grid/cells/text 88 | 89 | 90 | 91 | 92 | 93 | 94 | text 95 | ui/grid/cells/html 96 | 97 | 98 | 99 | 100 | 101 | 102 | textRange 103 | ui/grid/cells/text 104 | 105 | 106 | 107 | 108 | 109 | 110 | textRange 111 | ui/grid/cells/text 112 | 113 | 114 | 115 | 116 | 117 | 118 | textRange 119 | ui/grid/cells/text 120 | 121 | 122 | 123 | 124 | 125 | 126 | text 127 | ui/grid/cells/html 128 | 129 | 130 | 131 | 132 | 133 | 134 | event_id 135 | 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /view/adminhtml/ui_component/fredden_javascripterrorreporting_summary.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | fredden_javascripterrorreporting_summary.fredden_javascripterrorreporting_data_source 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | fredden_javascripterrorreporting_summary_columns 18 | 19 | fredden_javascripterrorreporting_summary.fredden_javascripterrorreporting_data_source 20 | 21 | 22 | 23 | 24 | 25 | 26 | event_id 27 | 28 | 29 | 30 | 31 | 32 | event_id 33 | event_id 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | false 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | date 50 | false 51 | false 52 | 53 | 54 | 55 | 56 | 57 | 58 | date 59 | false 60 | false 61 | 62 | 63 | 64 | 65 | 66 | 67 | ui/grid/cells/text 68 | false 69 | false 70 | 71 | 72 | 73 | 74 | 75 | 76 | ui/grid/cells/html 77 | false 78 | false 79 | 80 | 81 | 82 | 83 | 84 | 85 | ui/grid/cells/text 86 | false 87 | false 88 | 89 | 90 | 91 | 92 | 93 | 94 | ui/grid/cells/text 95 | false 96 | false 97 | 98 | 99 | 100 | 101 | 102 | 103 | text 104 | ui/grid/cells/html 105 | 106 | 107 | 108 | 109 | 110 | 111 | event_id 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /view/adminhtml/web/chart.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | 3 | define([ 4 | 'chartJs', 5 | ], function (Chart) { 6 | 'use strict'; 7 | 8 | return function (config, element) { 9 | new Chart(element, { 10 | type: 'line', 11 | data: { 12 | labels: config.data.labels, 13 | datasets: [{ 14 | label: '# of Errors', 15 | data: config.data.values, 16 | // These colours are from Magento_Backend::js/dashboard/chart.js 17 | backgroundColor: '#f1d4b3', 18 | borderColor: '#eb5202', 19 | borderWidth: 1, 20 | // Preserve styles from Charts.js v2.9.3 21 | cubicInterpolationMode: 'monotone', 22 | fill: true, 23 | }] 24 | }, 25 | options: { 26 | plugins: { 27 | legend: { 28 | display: false, 29 | }, 30 | title: { 31 | display: true, 32 | text: config.title, 33 | }, 34 | }, 35 | scales: { 36 | x: { 37 | display: false, 38 | }, 39 | y: { 40 | beginAtZero: true, 41 | }, 42 | }, 43 | }, 44 | }); 45 | }; 46 | }); 47 | -------------------------------------------------------------------------------- /view/base/layout/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /view/base/templates/script.phtml: -------------------------------------------------------------------------------- 1 | 2 | getViewFileUrl('Fredden_JavaScriptErrorReporting::error-handler.js'); ?> 3 | getApiUrl()); ?> 4 | 6 | -------------------------------------------------------------------------------- /view/base/web/error-handler.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var url = (document.currentScript && document.currentScript.dataset && document.currentScript.dataset.reportUrl) || 5 | ((window.BASE_URL || '') + '/rest/V1/fredden/javascript-error-reporting'); 6 | 7 | var sendDelay = 0; 8 | var sendQueue = []; 9 | var sendTimer; 10 | 11 | var sendError = function () { 12 | var xhr; 13 | 14 | window.clearTimeout(sendTimer); 15 | 16 | if (!sendQueue.length) { 17 | return; 18 | } 19 | 20 | xhr = new XMLHttpRequest(); 21 | xhr.open('POST', url, true); 22 | xhr.setRequestHeader('Content-Type', 'application/json'); 23 | xhr.send(JSON.stringify(sendQueue.shift())); 24 | 25 | sendDelay += 10; 26 | sendTimer = window.setTimeout(sendError, sendDelay); 27 | }; 28 | 29 | window.addEventListener('error', function (event) { 30 | if (!event) { 31 | return; 32 | } 33 | 34 | if (sendQueue.length > 100) { 35 | // That's a lot of errors! Let's not overwhelm the server with more. 36 | return; 37 | } 38 | 39 | if (!sendQueue.length) { 40 | sendTimer = window.setTimeout(sendError, sendDelay); 41 | } 42 | 43 | sendQueue.push({ 44 | browser: { 45 | height: window.innerHeight, 46 | url: window.location.href, 47 | width: window.innerWidth, 48 | }, 49 | event: { 50 | colno: event.colno, 51 | filename: event.filename, 52 | lineno: event.lineno, 53 | message: event.message, 54 | stack: event.error && event.error.stack, 55 | timer: (performance.now() / 1000).toFixed(2), 56 | }, 57 | }); 58 | }); 59 | }()); 60 | --------------------------------------------------------------------------------