',
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 |
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 | = $escaper->escapeHtml(__('The above list has been filtered to ignore %1 errors.', $count)) ?>
8 |
9 |
10 |
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 |
169 |
--------------------------------------------------------------------------------
/view/adminhtml/ui_component/fredden_javascripterrorreporting_grid.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | -
6 |
- fredden_javascripterrorreporting_grid.fredden_javascripterrorreporting_data_source
7 |
8 |
9 |
10 |
11 |
12 |
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 |
--------------------------------------------------------------------------------