├── src
├── web
│ └── assets
│ │ └── dist
│ │ ├── assets
│ │ ├── webperf-8z1FbwJh.js
│ │ ├── webperf-8z1FbwJh.js.map
│ │ ├── alerts-D3VsK7uo.js.gz
│ │ ├── sidebar-DyQeHXoP.js.gz
│ │ ├── webperf-zfG7r2xS.css.gz
│ │ ├── alerts-D3VsK7uo.js.map.gz
│ │ ├── dashboard-B8Tl2RKD.js.gz
│ │ ├── sidebar-DyQeHXoP.js.map.gz
│ │ ├── dashboard-B8Tl2RKD.js.map.gz
│ │ ├── errors-detail-CbuM4Hh8.js.gz
│ │ ├── errors-index-zYZol8PP.js.gz
│ │ ├── PageResultCell-b43rcj4h.js.gz
│ │ ├── RequestBarChart-Ee0Vh_Iz.js.gz
│ │ ├── SamplePaneFooter-DUo1brgB.js.gz
│ │ ├── SimpleBarChart-DDG34REw.js.gz
│ │ ├── errors-index-zYZol8PP.js.map.gz
│ │ ├── vue-apexcharts-C2g27_eS.js.gz
│ │ ├── DataSampleDate-UiLkVbOT.js.map.gz
│ │ ├── PageResultCell-b43rcj4h.js.map.gz
│ │ ├── SampleRangePicker-B5MMA2lb.js.gz
│ │ ├── SampleRangePicker-l4T-d3Wl.css.gz
│ │ ├── SimpleBarChart-DDG34REw.js.map.gz
│ │ ├── errors-detail-CbuM4Hh8.js.map.gz
│ │ ├── performance-detail-erdgCHrY.js.gz
│ │ ├── performance-index-Bj4b5YCn.js.gz
│ │ ├── vue-apexcharts-C2g27_eS.js.map.gz
│ │ ├── DataSampleDevice-D3lfOt8n.js.map.gz
│ │ ├── ErrorsDetailAreaChart-CoSEsohF.js.gz
│ │ ├── RecommendationsList-T8Et27n2.js.gz
│ │ ├── RequestBarChart-Ee0Vh_Iz.js.map.gz
│ │ ├── SamplePaneFooter-DUo1brgB.js.map.gz
│ │ ├── SampleRangePicker-B5MMA2lb.js.map.gz
│ │ ├── performance-index-Bj4b5YCn.js.map.gz
│ │ ├── tri-color-blend-CUFlaG2k.js.map.gz
│ │ ├── RecommendationsList-T8Et27n2.js.map.gz
│ │ ├── performance-detail-erdgCHrY.js.map.gz
│ │ ├── ErrorsDetailAreaChart-CoSEsohF.js.map.gz
│ │ ├── PerformanceDetailAreaChart-BW1ni5m3.js.gz
│ │ ├── PerformanceDetailAreaChart-BW1ni5m3.js.map.gz
│ │ ├── DataSampleDate-UiLkVbOT.js
│ │ ├── SampleSizeWarning-l8RSl_wj.js
│ │ ├── DataSampleDevice-D3lfOt8n.js
│ │ ├── tri-color-blend-CUFlaG2k.js
│ │ ├── SampleSizeWarning-l8RSl_wj.js.map
│ │ ├── PageResultCell-aK_k3t68.css
│ │ ├── DataSampleDate-UiLkVbOT.js.map
│ │ ├── DataSampleDevice-D3lfOt8n.js.map
│ │ ├── RecommendationsList-T8Et27n2.js
│ │ ├── ErrorsDetailAreaChart-CoSEsohF.js
│ │ ├── SamplePaneFooter-DUo1brgB.js
│ │ ├── PerformanceDetailAreaChart-BW1ni5m3.js
│ │ ├── tri-color-blend-CUFlaG2k.js.map
│ │ ├── RequestBarChart-Ee0Vh_Iz.js
│ │ ├── SimpleBarChart-DDG34REw.js
│ │ ├── SamplePaneFooter-DUo1brgB.js.map
│ │ └── RecommendationsList-T8Et27n2.js.map
│ │ ├── manifest.json.gz
│ │ └── img
│ │ └── Webperf-icon.svg
├── lib
│ └── geoiploc.php
├── templates
│ ├── _includes
│ │ ├── range-picker.twig
│ │ ├── macros.twig
│ │ ├── speed-color-key.twig
│ │ ├── recommendations.twig
│ │ └── sites-menu.twig
│ ├── _frontend
│ │ └── scripts
│ │ │ ├── load-boomerang-amp-iframe.twig
│ │ │ ├── webperf-config.twig
│ │ │ ├── boomerang-amp-iframe-html.twig
│ │ │ ├── webperf-boomerang-custom.twig
│ │ │ └── capture-errors.twig
│ ├── _layouts
│ │ ├── widget-cp.twig
│ │ └── webperf-cp.twig
│ ├── _components
│ │ └── widgets
│ │ │ ├── Metrics_body.twig
│ │ │ └── Metrics_settings.twig
│ ├── alerts
│ │ └── index.twig
│ ├── settings
│ │ ├── index.twig
│ │ └── _includes
│ │ │ ├── errors.twig
│ │ │ ├── appearance.twig
│ │ │ ├── performance.twig
│ │ │ └── general.twig
│ ├── errors
│ │ ├── index.twig
│ │ └── page-detail.twig
│ ├── performance
│ │ └── index.twig
│ └── dashboard
│ │ └── index.twig
├── assetbundles
│ ├── boomerang
│ │ ├── src
│ │ │ ├── js
│ │ │ │ └── webperf-boomer-init.js
│ │ │ └── json
│ │ │ │ └── plugins.json
│ │ ├── BoomerangAsset.php
│ │ └── build-boomerang.sh
│ └── webperf
│ │ ├── WebperfAsset.php
│ │ └── WebperfDashboardAsset.php
├── base
│ ├── RecommendationInterface.php
│ ├── BoomerangDataSample.php
│ ├── CraftDataSample.php
│ ├── DbDataSampleInterface.php
│ ├── DbErrorSampleInterface.php
│ ├── Recommendation.php
│ ├── RecommendationTrait.php
│ ├── CleanModel.php
│ ├── FluentModel.php
│ ├── DbErrorSample.php
│ ├── DbDataSampleTrait.php
│ ├── CraftDataSampleTrait.php
│ ├── DbErrorSampleTrait.php
│ └── BoomerangDataSampleTrait.php
├── events
│ ├── ErrorSampleEvent.php
│ ├── DataSampleEvent.php
│ └── AlertEvent.php
├── models
│ ├── CraftDbErrorSample.php
│ ├── BoomerangDbErrorSample.php
│ ├── RecommendationDataSample.php
│ ├── CraftDbDataSample.php
│ └── BoomerangDbDataSample.php
├── migrations
│ └── m190625_151715_add_indexes.php
├── helpers
│ ├── Permission.php
│ ├── PluginTemplate.php
│ └── Text.php
├── validators
│ └── DbStringValidator.php
├── controllers
│ ├── RenderController.php
│ ├── RecommendationsController.php
│ ├── DataSamplesController.php
│ ├── ErrorSamplesController.php
│ ├── SettingsController.php
│ └── FileController.php
├── icon-mask.svg
├── recommendations
│ ├── CraftTotalTime.php
│ ├── CraftQueryCount.php
│ ├── CraftQueryTime.php
│ ├── CraftTwigTime.php
│ ├── DomInteractive.php
│ ├── FirstByte.php
│ ├── FirstContentfulPaint.php
│ └── MemoryLimit.php
├── icon.svg
├── console
│ └── controllers
│ │ └── SamplesController.php
├── records
│ └── Alerts.php
├── variables
│ └── WebperfVariable.php
├── log
│ └── ErrorsTarget.php
├── widgets
│ └── Metrics.php
└── services
│ ├── ServicesTrait.php
│ └── Recommendations.php
├── phpstan.neon
├── ecs.php
├── CHANGELOG.md
├── composer.json
├── README.md
└── LICENSE.md
/src/web/assets/dist/assets/webperf-8z1FbwJh.js:
--------------------------------------------------------------------------------
1 |
2 | //# sourceMappingURL=webperf-8z1FbwJh.js.map
3 |
--------------------------------------------------------------------------------
/src/lib/geoiploc.php:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/lib/geoiploc.php
--------------------------------------------------------------------------------
/src/web/assets/dist/manifest.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/manifest.json.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/webperf-8z1FbwJh.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"webperf-8z1FbwJh.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/src/templates/_includes/range-picker.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/alerts-D3VsK7uo.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/alerts-D3VsK7uo.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/sidebar-DyQeHXoP.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/sidebar-DyQeHXoP.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/webperf-zfG7r2xS.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/webperf-zfG7r2xS.css.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/alerts-D3VsK7uo.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/alerts-D3VsK7uo.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/dashboard-B8Tl2RKD.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/dashboard-B8Tl2RKD.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/sidebar-DyQeHXoP.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/sidebar-DyQeHXoP.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/dashboard-B8Tl2RKD.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/dashboard-B8Tl2RKD.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/errors-detail-CbuM4Hh8.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/errors-detail-CbuM4Hh8.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/errors-index-zYZol8PP.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/errors-index-zYZol8PP.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/PageResultCell-b43rcj4h.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/PageResultCell-b43rcj4h.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/RequestBarChart-Ee0Vh_Iz.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/RequestBarChart-Ee0Vh_Iz.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SamplePaneFooter-DUo1brgB.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/SamplePaneFooter-DUo1brgB.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SimpleBarChart-DDG34REw.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/SimpleBarChart-DDG34REw.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/errors-index-zYZol8PP.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/errors-index-zYZol8PP.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/vue-apexcharts-C2g27_eS.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/vue-apexcharts-C2g27_eS.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/DataSampleDate-UiLkVbOT.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/DataSampleDate-UiLkVbOT.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/PageResultCell-b43rcj4h.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/PageResultCell-b43rcj4h.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SampleRangePicker-B5MMA2lb.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/SampleRangePicker-B5MMA2lb.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SampleRangePicker-l4T-d3Wl.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/SampleRangePicker-l4T-d3Wl.css.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SimpleBarChart-DDG34REw.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/SimpleBarChart-DDG34REw.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/errors-detail-CbuM4Hh8.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/errors-detail-CbuM4Hh8.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/performance-detail-erdgCHrY.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/performance-detail-erdgCHrY.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/performance-index-Bj4b5YCn.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/performance-index-Bj4b5YCn.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/vue-apexcharts-C2g27_eS.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/vue-apexcharts-C2g27_eS.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/DataSampleDevice-D3lfOt8n.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/DataSampleDevice-D3lfOt8n.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/ErrorsDetailAreaChart-CoSEsohF.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/ErrorsDetailAreaChart-CoSEsohF.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/RecommendationsList-T8Et27n2.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/RecommendationsList-T8Et27n2.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/RequestBarChart-Ee0Vh_Iz.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/RequestBarChart-Ee0Vh_Iz.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SamplePaneFooter-DUo1brgB.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/SamplePaneFooter-DUo1brgB.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SampleRangePicker-B5MMA2lb.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/SampleRangePicker-B5MMA2lb.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/performance-index-Bj4b5YCn.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/performance-index-Bj4b5YCn.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/tri-color-blend-CUFlaG2k.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/tri-color-blend-CUFlaG2k.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/RecommendationsList-T8Et27n2.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/RecommendationsList-T8Et27n2.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/performance-detail-erdgCHrY.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/performance-detail-erdgCHrY.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/ErrorsDetailAreaChart-CoSEsohF.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/ErrorsDetailAreaChart-CoSEsohF.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/PerformanceDetailAreaChart-BW1ni5m3.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/PerformanceDetailAreaChart-BW1ni5m3.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/PerformanceDetailAreaChart-BW1ni5m3.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-webperf/develop-v5/src/web/assets/dist/assets/PerformanceDetailAreaChart-BW1ni5m3.js.map.gz
--------------------------------------------------------------------------------
/src/assetbundles/boomerang/src/js/webperf-boomer-init.js:
--------------------------------------------------------------------------------
1 | // This code is run after all plugins have initialized
2 | BOOMR.init({
3 | beacon_url: '/webperf/metrics/beacon',
4 | log: null,
5 | });
6 | BOOMR.t_end = new Date().getTime();
7 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon
3 |
4 | parameters:
5 | level: 5
6 | paths:
7 | - src
8 | excludePaths:
9 | analyse:
10 | - src/lib/*
11 |
--------------------------------------------------------------------------------
/src/templates/_frontend/scripts/load-boomerang-amp-iframe.twig:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assetbundles/boomerang/src/json/plugins.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "plugins/rt.js",
4 | "plugins/painttiming.js",
5 | "plugins/navtiming.js",
6 | "plugins/errors.js",
7 | "plugins/md5.js",
8 | "plugins/mq.js",
9 | "plugins/webperf-boomer-init.js"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/templates/_frontend/scripts/webperf-config.twig:
--------------------------------------------------------------------------------
1 | {% if headless is not defined or not headless %}
2 | var webperf_config = {
3 | url: '{{ boomerangScriptUrl | raw }}',
4 | title: '{{ boomerangTitle | raw }}' || document.title || '',
5 | requestId: '{{ boomerangRequestId | raw }}',
6 | };
7 | {% endif %}
8 |
--------------------------------------------------------------------------------
/src/templates/_layouts/widget-cp.twig:
--------------------------------------------------------------------------------
1 | {% block head %}
2 | {% set tagOptions = {
3 | 'depends': [
4 | 'nystudio107\\webperf\\assetbundles\\webperf\\WebperfAsset'
5 | ],
6 | } %}
7 | {{ craft.webperf.register('src/js/webperf.js', false, tagOptions, tagOptions) }}
8 | {% endblock %}
9 |
10 | {% block content %}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/src/templates/_layouts/webperf-cp.twig:
--------------------------------------------------------------------------------
1 | {% extends "_layouts/cp" %}
2 |
3 | {% block head %}
4 | {{ parent() }}
5 | {% set tagOptions = {
6 | 'depends': [
7 | 'nystudio107\\webperf\\assetbundles\\webperf\\WebperfAsset'
8 | ],
9 | } %}
10 | {{ craft.webperf.register('src/js/webperf.js', false, tagOptions, tagOptions) }}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/src/templates/_includes/macros.twig:
--------------------------------------------------------------------------------
1 | {% macro configWarning(setting, file) -%}
2 | {%- set configArray = craft.app.config.getConfigFromFile(file) -%}
3 | {%- if configArray[setting] is defined -%}
4 | {{- "This is being overridden by the `#{setting}` setting in the `config/#{file}.php` file." |raw }}
5 | {%- else -%}
6 | {{ false }}
7 | {%- endif -%}
8 | {%- endmacro %}
9 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | paths([
8 | __DIR__ . '/src',
9 | __FILE__,
10 | ]);
11 | $ecsConfig->skip([
12 | __DIR__ . '/src/lib',
13 | ]);
14 | $ecsConfig->parallel();
15 | $ecsConfig->sets([SetList::CRAFT_CMS_4]);
16 | };
17 |
18 |
--------------------------------------------------------------------------------
/src/templates/_frontend/scripts/boomerang-amp-iframe-html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/DataSampleDate-UiLkVbOT.js:
--------------------------------------------------------------------------------
1 | import{n as a}from"./vue-apexcharts-C2g27_eS.js";const n={name:"DataSampleDate",props:{date:{type:String,default:""},url:{type:String,default:""},query:{type:String,default:""}},computed:{title(){let e="";return e+="Url: "+this.url,this.query&&(e+=`
2 |
3 | Query: `+this.query),e}}};var l=function(){var t=this,r=t._self._c;return r("span",{staticClass:"cursor-default",attrs:{title:t.title}},[t._v(t._s(t.date))])},s=[],u=a(n,l,s,!1,null,null,null,null);const _=u.exports;export{_ as D};
4 | //# sourceMappingURL=DataSampleDate-UiLkVbOT.js.map
5 |
--------------------------------------------------------------------------------
/src/templates/_components/widgets/Metrics_body.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Webperf plugin for Craft CMS
5 | *
6 | * DataSamples Widget Body
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2019 nystudio107
10 | * @link https://nystudio107.com
11 | * @package Webperf
12 | * @since 1.0.0
13 | */
14 | #}
15 |
16 | {% set iconUrl = view.getAssetManager().getPublishedUrl('@nystudio107/webperf/assetbundles/metricswidget/dist', true) ~ '/img/DataSamples-icon.svg' %}
17 |
18 |
19 |
20 | {{ message }}
21 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Webperf Changelog
2 |
3 | ## 5.0.0 - 2024.04.16
4 | ### Added
5 | * Stable release for Craft CMS 5
6 |
7 | ### Fixed
8 | * Fixed an issue where the down and up arrows were reversed for sorting purposes
9 |
10 | ## 5.0.0-beta.3 - 2024.02.09
11 | ### Fixed
12 | * Fixed an issue with the Sites menu styling
13 |
14 | ## 5.0.0-beta.2 - 2024.02.09
15 | ### Fixed
16 | * Added the unused `static` to the Tailwind CSS `blocklist` to avoid a name collision with a Craft CSS class ([#1412](https://github.com/nystudio107/craft-seomatic/issues/1412))
17 |
18 | ## 5.0.0-beta.1 - 2024.02.08
19 | ### Added
20 | * Initial beta release for Craft CMS 5
21 |
--------------------------------------------------------------------------------
/src/templates/_includes/speed-color-key.twig:
--------------------------------------------------------------------------------
1 |
2 |
5 | Fast
6 | Average
7 | Slow
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SampleSizeWarning-l8RSl_wj.js:
--------------------------------------------------------------------------------
1 | import{n as s}from"./vue-apexcharts-C2g27_eS.js";const n={name:"SampleSizeWarning",props:{sample:{type:Number,default:0}}};var l=function(){var e=this,a=e._self._c;return a("div",{staticClass:"field webperf-tooltip text-sm font-normal inline-block"},[e.sample<100?a("p",{staticClass:"warning display-block"},[e._v(" ")]):e._e(),a("span",{staticClass:"webperf-tooltiptext webperf-sample-tooltip"},[e._v(" Only "+e._s(e.sample)+" data sample"),e.sample!==1?a("span",[e._v("s")]):e._e(),e._v(". ")])])},t=[],p=s(n,l,t,!1,null,null,null,null);const i=p.exports;export{i as S};
2 | //# sourceMappingURL=SampleSizeWarning-l8RSl_wj.js.map
3 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/DataSampleDevice-D3lfOt8n.js:
--------------------------------------------------------------------------------
1 | import{n as s}from"./vue-apexcharts-C2g27_eS.js";const l={name:"DataSampleDevice",props:{device:{type:String,default:""},mobile:{type:Boolean,default:!1}},computed:{className(){let e="";return this.device&&this.mobile!==void 0&&(e=this.mobile===!0?"webperf-mobile-icon":"webperf-desktop-icon"),e},title(){let e="";return this.device&&this.mobile!==void 0&&(e=this.mobile===!0?"Mobile device":"Desktop device"),e}}};var a=function(){var t=this,i=t._self._c;return i("span",{staticClass:"cursor-default",class:t.className,attrs:{title:t.title}},[t._v(" "+t._s(t.device))])},n=[],r=s(l,a,n,!1,null,null,null,null);const c=r.exports;export{c as D};
2 | //# sourceMappingURL=DataSampleDevice-D3lfOt8n.js.map
3 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/tri-color-blend-CUFlaG2k.js:
--------------------------------------------------------------------------------
1 | class c{constructor(t="#00C800",r="#FFFF00",e="#C80000"){this.clr1=this.HexToRGB(t),this.clr2=this.HexToRGB(r),this.clr3=this.HexToRGB(e)}RGBToHex(t,r,e){let o=t<<16|r<<8|e;return function(n){return new Array(7-n.length).join("0")+n}(o.toString(16).toUpperCase())}HexToRGB(t){let r=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return r?{r:parseInt(r[1],16),g:parseInt(r[2],16),b:parseInt(r[3],16)}:null}colorFromPercentage(t){let r=this.clr1,e=this.clr2;t>=50&&(r=this.clr2,e=this.clr3,t=t-50);const o=t/50,n=Math.round(r.r+o*(e.r-r.r)),s=Math.round(r.g+o*(e.g-r.g)),i=Math.round(r.b+o*(e.b-r.b));return"#"+this.RGBToHex(n,s,i)}}export{c as T};
2 | //# sourceMappingURL=tri-color-blend-CUFlaG2k.js.map
3 |
--------------------------------------------------------------------------------
/src/templates/_components/widgets/Metrics_settings.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Webperf plugin for Craft CMS
5 | *
6 | * DataSamples Widget Settings
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2019 nystudio107
10 | * @link https://nystudio107.com
11 | * @package Webperf
12 | * @since 1.0.0
13 | */
14 | #}
15 |
16 | {% import "_includes/forms" as forms %}
17 |
18 | {% do view.registerAssetBundle("nystudio107\\webperf\\assetbundles\\webperf\\WebperfAsset") %}
19 |
20 | {{ forms.textField({
21 | label: 'Message',
22 | instructions: 'Enter a message here.',
23 | id: 'message',
24 | name: 'message',
25 | value: widget['message']})
26 | }}
27 |
--------------------------------------------------------------------------------
/src/base/RecommendationInterface.php:
--------------------------------------------------------------------------------
1 | \n \n
\n \n
\n
\n Only {{ sample }} data samples .\n \n
\n\n\n"],"names":["_sfc_main"],"mappings":"iDAcA,MAAAA,EAAA,CACA,KAAA,oBACA,MAAA,CACA,OAAA,CACA,KAAA,OACA,QAAA,CACA,CACA,CACA"}
--------------------------------------------------------------------------------
/src/models/CraftDbErrorSample.php:
--------------------------------------------------------------------------------
1 | type = 'craft';
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/models/BoomerangDbErrorSample.php:
--------------------------------------------------------------------------------
1 | type = 'boomerang';
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/events/DataSampleEvent.php:
--------------------------------------------------------------------------------
1 | sourcePath = "@nystudio107/webperf/assetbundles/boomerang/dist";
31 |
32 | $this->js = [
33 | 'js/boomerang-1.0.0.min.js',
34 | ];
35 |
36 | parent::init();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/DataSampleDate-UiLkVbOT.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"DataSampleDate-UiLkVbOT.js","sources":["../../../../../buildchain/src/vue/tables/common/DataSampleDate.vue"],"sourcesContent":["\n {{ date }} \n \n\n"],"names":["_sfc_main","title"],"mappings":"iDAOA,MAAAA,EAAA,CACA,KAAA,iBACA,MAAA,CACA,KAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,IAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,MAAA,CACA,KAAA,OACA,QAAA,EACA,CACA,EACA,SAAA,CACA,OAAA,CACA,IAAAC,EAAA,GAEA,OAAAA,GAAA,QAAA,KAAA,IACA,KAAA,QACAA,GAAA;AAAA;AAAA,SAAA,KAAA,OAGAA,CACA,CACA,CACA"}
--------------------------------------------------------------------------------
/src/base/BoomerangDataSample.php:
--------------------------------------------------------------------------------
1 | boomerangRules(),
38 | [
39 | ]
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/events/AlertEvent.php:
--------------------------------------------------------------------------------
1 | sourcePath = "@nystudio107/webperf/web/assets/dist";
33 |
34 | $this->depends = [
35 | CpAsset::class,
36 | VueAsset::class,
37 | ];
38 |
39 | $this->js = [
40 | ];
41 |
42 | $this->css = [
43 | ];
44 |
45 | parent::init();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/assetbundles/webperf/WebperfDashboardAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = "@nystudio107/webperf/web/assets/dist";
33 |
34 | $this->depends = [
35 | CpAsset::class,
36 | VueAsset::class,
37 | WebperfAsset::class,
38 | ];
39 |
40 | $this->js = [
41 | ];
42 |
43 | $this->css = [
44 | ];
45 |
46 | parent::init();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/base/CraftDataSample.php:
--------------------------------------------------------------------------------
1 | craftRules(),
43 | [
44 | ]
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/base/DbDataSampleInterface.php:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
Recommendations
20 |
28 |
29 |
30 |
31 |
32 | {% endif %}
33 |
--------------------------------------------------------------------------------
/src/base/DbErrorSampleInterface.php:
--------------------------------------------------------------------------------
1 | sample) {
35 | $this->evaluate();
36 | }
37 | }
38 |
39 | /**
40 | * Display the passed in ms in seconds, to two decimal places
41 | *
42 | * @param int $number
43 | *
44 | * @return string
45 | */
46 | public function displayMs(int $number): string
47 | {
48 | return number_format((float)$number / 1000, 2) . 's';
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/DataSampleDevice-D3lfOt8n.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"DataSampleDevice-D3lfOt8n.js","sources":["../../../../../buildchain/src/vue/tables/common/DataSampleDevice.vue"],"sourcesContent":["\n {{ device }} \n \n\n"],"names":["_sfc_main","className","title"],"mappings":"iDAQA,MAAAA,EAAA,CACA,KAAA,mBACA,MAAA,CACA,OAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,OAAA,CACA,KAAA,QACA,QAAA,EACA,CACA,EACA,SAAA,CACA,WAAA,CACA,IAAAC,EAAA,GAEA,OAAA,KAAA,QAAA,KAAA,SAAA,SACAA,EAAA,KAAA,SAAA,GAAA,sBAAA,wBAGAA,CACA,EACA,OAAA,CACA,IAAAC,EAAA,GAEA,OAAA,KAAA,QAAA,KAAA,SAAA,SACAA,EAAA,KAAA,SAAA,GAAA,gBAAA,kBAGAA,CACA,CACA,CACA"}
--------------------------------------------------------------------------------
/src/migrations/m190625_151715_add_indexes.php:
--------------------------------------------------------------------------------
1 | createIndex(
19 | $this->db->getIndexName(),
20 | '{{%webperf_data_samples}}',
21 | 'dateCreated',
22 | false
23 | );
24 | $this->createIndex(
25 | $this->db->getIndexName(),
26 | '{{%webperf_data_samples}}',
27 | 'requestId',
28 | false
29 | );
30 | // Add webperf_error_samples indexes
31 | $this->createIndex(
32 | $this->db->getIndexName(),
33 | '{{%webperf_error_samples}}',
34 | 'dateCreated',
35 | false
36 | );
37 | $this->createIndex(
38 | $this->db->getIndexName(),
39 | '{{%webperf_error_samples}}',
40 | 'requestId',
41 | false
42 | );
43 | }
44 |
45 | /**
46 | * @inheritdoc
47 | */
48 | public function safeDown()
49 | {
50 | echo "m190625_151715_add_indexes cannot be reverted.\n";
51 | return false;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/helpers/Permission.php:
--------------------------------------------------------------------------------
1 | getUser()->getIdentity()) === null) {
38 | throw new ForbiddenHttpException('Your account has no identity.');
39 | }
40 |
41 | if (!$currentUser->can($permission)) {
42 | throw new ForbiddenHttpException("Your account doesn't have permission to assign access this resource.");
43 | }
44 | }
45 |
46 | // Protected Static Methods
47 | // =========================================================================
48 | }
49 |
--------------------------------------------------------------------------------
/src/validators/DbStringValidator.php:
--------------------------------------------------------------------------------
1 | max === null) {
39 | throw new InvalidConfigException('The "max" property must be set.');
40 | }
41 | }
42 |
43 | /**
44 | * @inheritdoc
45 | */
46 | public function validateAttribute($model, $attribute): void
47 | {
48 | $value = $model->$attribute;
49 | $value = TextHelper::cleanupText($value);
50 | $value = StringHelper::encodeMb4($value);
51 | $value = TextHelper::truncate($value, $this->max);
52 | $model->$attribute = $value;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/base/RecommendationTrait.php:
--------------------------------------------------------------------------------
1 | asRaw(Webperf::$plugin->beacons->ampHtmlIframe());
41 | }
42 |
43 | /**
44 | * @param string $title
45 | *
46 | * @return Response
47 | */
48 | public function actionBeaconScript(string $title = ''): Response
49 | {
50 | return $this->asRaw(Webperf::$plugin->beacons->htmlBeaconScript(true, $title));
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/base/CleanModel.php:
--------------------------------------------------------------------------------
1 | $propValue) {
47 | if (!property_exists($class, $propName)) {
48 | unset($config[$propName]);
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/templates/_frontend/scripts/webperf-boomerang-custom.twig:
--------------------------------------------------------------------------------
1 | // Modern browsers
2 | if (document.addEventListener) {
3 | document.addEventListener("onBoomerangLoaded", function(e) {
4 | // e.detail.BOOMR is a reference to the BOOMR global object
5 | if (webperf_config.title) {
6 | e.detail.BOOMR.addVar({
7 | 'doc_title': webperf_config.title,
8 | });
9 | }
10 | if (webperf_config.requestId) {
11 | e.detail.BOOMR.addVar({
12 | 'request_id': webperf_config.requestId,
13 | });
14 | }
15 | });
16 | }
17 | // IE 6, 7, 8 we use onPropertyChange and look for propertyName === "onBoomerangLoaded"
18 | else if (document.attachEvent) {
19 | document.attachEvent("onpropertychange", function(e) {
20 | if (!e) e=event;
21 | if (e.propertyName === "onBoomerangLoaded") {
22 | // e.detail.BOOMR is a reference to the BOOMR global object
23 | if (webperf_config.title) {
24 | e.detail.BOOMR.addVar({
25 | 'doc_title': webperf_config.title,
26 | });
27 | }
28 | if (webperf_config.requestId) {
29 | e.detail.BOOMR.addVar({
30 | 'request_id': webperf_config.requestId,
31 | });
32 | }
33 | }
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/src/assetbundles/boomerang/build-boomerang.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | BOOMERANG_BASE_DIR="../../../node_modules/boomerangjs"
3 | BOOMERANG_PLUGINS_JSON_DIR="src/json"
4 | BOOMERANG_PLUGINS_JS_DIR="src/js"
5 | BOOMERANG_DIST_DIR="dist/js"
6 | WORKING_DIR=$(pwd)
7 | PLUGINS_FILE="plugins.json"
8 | COPY_PLUGIN_JS_FILES=(
9 | "webperf-boomer-init.js"
10 | )
11 | COPY_BUILD_FILES=(
12 | "boomerang-1.0.0.min.js"
13 | "boomerang-1.0.0.min.js.map"
14 | )
15 |
16 | if [[ ! -f "${BOOMERANG_BASE_DIR}/${PLUGINS_FILE}.bak" ]]; then
17 | echo "Backing up ${BOOMERANG_BASE_DIR}/${PLUGINS_FILE}"
18 | cp "${BOOMERANG_BASE_DIR}/${PLUGINS_FILE}" "${BOOMERANG_BASE_DIR}/${PLUGINS_FILE}.bak"
19 | fi
20 |
21 | echo "Copying ${BOOMERANG_PLUGINS_JSON_DIR}/${PLUGINS_FILE} to ${BOOMERANG_BASE_DIR}/${PLUGINS_FILE}"
22 | cp "${BOOMERANG_PLUGINS_JSON_DIR}/${PLUGINS_FILE}" "${BOOMERANG_BASE_DIR}/${PLUGINS_FILE}"
23 |
24 | for file in "${COPY_PLUGIN_JS_FILES[@]}"
25 | do
26 | echo "Copying ${BOOMERANG_PLUGINS_JS_DIR}/${file} to ${BOOMERANG_BASE_DIR}/plugins/${file}"
27 | cp "${BOOMERANG_PLUGINS_JS_DIR}/${file}" "${BOOMERANG_BASE_DIR}/plugins/${file}"
28 | done
29 |
30 | echo "Building Boomerang"
31 | cd "${BOOMERANG_BASE_DIR}"
32 | grunt clean build --build-number="0" --build-revision="0"
33 |
34 | cd "${WORKING_DIR}"
35 | for file in "${COPY_BUILD_FILES[@]}"
36 | do
37 | echo "Copying ${BOOMERANG_BASE_DIR}/build/${file} to ${BOOMERANG_DIST_DIR}/${file}"
38 | cp "${BOOMERANG_BASE_DIR}/build/${file}" "${BOOMERANG_DIST_DIR}/${file}"
39 | done
40 |
41 | exit 0
42 |
--------------------------------------------------------------------------------
/src/base/FluentModel.php:
--------------------------------------------------------------------------------
1 | hasProperty($method)) {
38 | throw new InvalidArgumentException("Property {$method} doesn't exist");
39 | }
40 | $property = $reflector->getProperty($method);
41 | if (empty($args)) {
42 | // Return the property
43 | return $property->getValue();
44 | }
45 | // Set the property
46 | $property->setValue($this, $args[0]);
47 |
48 | // Make it chainable
49 | return $this;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/models/RecommendationDataSample.php:
--------------------------------------------------------------------------------
1 | craftRules(),
54 | $this->boomerangRules(),
55 | [
56 | ]
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/base/DbErrorSample.php:
--------------------------------------------------------------------------------
1 | dbErrorRules(),
43 | [
44 | ]
45 | );
46 | }
47 |
48 | /**
49 | * @return array
50 | */
51 | public function behaviors(): array
52 | {
53 | return array_merge(
54 | parent::behaviors(),
55 | [
56 | 'typecast' => [
57 | 'class' => AttributeTypecastBehavior::class,
58 | // 'attributeTypes' will be composed automatically according to `rules()`
59 | ],
60 | ]
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/icon-mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
17 |
18 |
--------------------------------------------------------------------------------
/src/templates/alerts/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Webperf plugin for Craft CMS 3.x
5 | *
6 | * Monitor the performance of your webpages through real-world user timing data
7 | *
8 | * @link https://nystudio107.com
9 | * @copyright Copyright (c) 2019 nystudio107
10 | */
11 | #}
12 |
13 | {% requirePermission "webperf:alerts" %}
14 |
15 | {% extends "webperf/_layouts/webperf-cp.twig" %}
16 |
17 | {% import "_includes/forms" as forms %}
18 |
19 | {% block contextMenu %}
20 | {% include "webperf/_includes/sites-menu.twig" with {'params': {}} %}
21 | {% endblock %}
22 |
23 | {% block actionButton %}
24 | {% endblock %}
25 |
26 | {% block content %}
27 |
32 |
33 | {{ csrfInput() }}
34 | {{ redirectInput("webperf/alerts") }}
35 |
36 |
37 |
38 | {% if not webhooks %}
39 |
40 |
The Webhooks plugin must be installed for the Alerts to function.
41 |
42 | {% endif %}
43 |
44 |
45 | {% endblock %}
46 |
47 | {% block foot %}
48 | {{ parent() }}
49 | {% set tagOptions = {
50 | 'depends': [
51 | 'nystudio107\\webperf\\assetbundles\\webperf\\WebperfAsset'
52 | ],
53 | } %}
54 | {{ craft.webperf.register('src/js/alerts.js', false, tagOptions, tagOptions) }}
55 | {% endblock %}
56 |
--------------------------------------------------------------------------------
/src/models/CraftDbDataSample.php:
--------------------------------------------------------------------------------
1 | dbRules(),
43 | [
44 | ]
45 | );
46 | }
47 |
48 | /**
49 | * @return array
50 | */
51 | public function behaviors(): array
52 | {
53 | return array_merge(
54 | parent::behaviors(),
55 | [
56 | 'typecast' => [
57 | 'class' => AttributeTypecastBehavior::class,
58 | // 'attributeTypes' will be composed automatically according to `rules()`
59 | ],
60 | ]
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/base/DbDataSampleTrait.php:
--------------------------------------------------------------------------------
1 | 120],
62 | ['url', DbStringValidator::class, 'max' => 255],
63 | ['queryString', DbStringValidator::class, 'max' => 255],
64 | ['title', 'string'],
65 | ['url', 'string'],
66 | ['queryString', 'string'],
67 | ];
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/templates/settings/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Webperf plugin for Craft CMS 3.x
5 | *
6 | * Monitor the performance of your webpages through real-world user timing data
7 | *
8 | * @link https://nystudio107.com
9 | * @copyright Copyright (c) 2019 nystudio107
10 | */
11 | #}
12 |
13 | {% requirePermission "webperf:settings" %}
14 |
15 | {% extends "_layouts/cp" %}
16 |
17 | {% import "_includes/forms" as forms %}
18 | {% from "webperf/_includes/macros.twig" import configWarning %}
19 |
20 | {% set tabs = {
21 | "general": {label: "General"|t('webperf'), url: "#general"},
22 | "performance": {label: "Performance"|t('webperf'), url: "#performance"},
23 | "errors": {label: "Errors"|t('webperf'), url: "#errors"},
24 | "appearance": {label: "Appearance"|t('webperf'), url: "#appearance"},
25 | } %}
26 |
27 | {% block content %}
28 |
29 |
30 |
31 |
32 | {{ redirectInput("webperf/settings") }}
33 |
34 | {# -- General settings -- #}
35 | {% include "webperf/settings/_includes/general.twig" %}
36 |
37 | {# -- Performance settings -- #}
38 | {% include "webperf/settings/_includes/performance.twig" %}
39 |
40 | {# -- Errors settings -- #}
41 | {% include "webperf/settings/_includes/errors.twig" %}
42 |
43 | {# -- Appearance settings -- #}
44 | {% include "webperf/settings/_includes/appearance.twig" %}
45 |
46 | {# -- Threshold settings -- #}
47 | {# Disabled from view for now
48 | {% include "webperf/settings/_includes/thresholds.twig" %}
49 | #}
50 |
51 | {# include our JavaScript modules #}
52 | {{ parent() }}
53 | {% endblock %}
54 |
--------------------------------------------------------------------------------
/src/recommendations/CraftTotalTime.php:
--------------------------------------------------------------------------------
1 | sample->craftTotalMs >= self::MAX_TOTAL_TIME) {
38 | $this->hasRecommendation = true;
39 | $this->summary = Craft::t(
40 | 'webperf',
41 | 'Look into utilizing the `{% cache %}` tag',
42 | []
43 | );
44 | $this->detail = Craft::t(
45 | 'webperf',
46 | 'It took Craft a total of {displayCraftTotalMs} to render. Ensure you are utilizing the `{% cache %}` tag effectively to solve concurrency issues.',
47 | [
48 | 'displayCraftTotalMs' => $this->displayMs($this->sample->craftTotalMs),
49 | ]
50 | );
51 | $this->learnMoreUrl = 'https://nystudio107.com/blog/the-craft-cache-tag-in-depth';
52 |
53 | return;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/recommendations/CraftQueryCount.php:
--------------------------------------------------------------------------------
1 | sample->craftDbCnt >= self::MAX_QUERIES) {
38 | $this->hasRecommendation = true;
39 | $this->summary = Craft::t(
40 | 'webperf',
41 | 'Look into Eager Loading to decrease database queries',
42 | []
43 | );
44 | $this->detail = Craft::t(
45 | 'webperf',
46 | 'A total of {dbQueries} database queries were executed. Look into decreasing the number of database queries needed by leveraging Eager Loading in Craft CMS.',
47 | [
48 | 'dbQueries' => round($this->sample->craftDbCnt),
49 | ]
50 | );
51 | $this->learnMoreUrl = 'https://nystudio107.com/blog/speed-up-your-craft-cms-templates-with-eager-loading';
52 |
53 | return;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/recommendations/CraftQueryTime.php:
--------------------------------------------------------------------------------
1 | sample->craftDbMs >= self::MAX_QUERY_TIME) {
38 | $this->hasRecommendation = true;
39 | $this->summary = Craft::t(
40 | 'webperf',
41 | 'Look into decreasing the time database queries are taking',
42 | []
43 | );
44 | $this->detail = Craft::t(
45 | 'webperf',
46 | 'The database queries took {displayCraftDbMs} to execute. Try to simplify the database queries, or leverage Eager Loading in Craft to speed them up.',
47 | [
48 | 'displayCraftDbMs' => $this->displayMs($this->sample->craftDbMs),
49 | ]
50 | );
51 | $this->learnMoreUrl = 'https://nystudio107.com/blog/speed-up-your-craft-cms-templates-with-eager-loading';
52 |
53 | return;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/templates/settings/_includes/errors.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 | {% from "webperf/_includes/macros.twig" import configWarning %}
3 |
4 | {% namespace "settings" %}
5 | {{ forms.lightswitchField({
6 | label: "Include Craft `warnings` as well as `errors`"|md|t("webperf"),
7 | instructions: "Whether Craft `warning` messages should be recorded in addition to `error` messages"|t("webperf"),
8 | id: "includeCraftWarnings",
9 | name: "includeCraftWarnings",
10 | on: settings.includeCraftWarnings,
11 | warning: configWarning("includeCraftWarnings", "webperf"),
12 | errors: settings.getErrors("includeCraftWarnings"),
13 | }) }}
14 |
15 | {{ forms.textField({
16 | label: "Error Samples to Store"|t("webperf"),
17 | instructions: "How many unique Error Samples should be stored before they are trimmed."|t("webperf"),
18 | id: "errorSamplesStoredLimit",
19 | name: "errorSamplesStoredLimit",
20 | size: 10,
21 | maxlength: 10,
22 | value: settings.errorSamplesStoredLimit,
23 | warning: configWarning("errorSamplesStoredLimit", "webperf"),
24 | errors: settings.getErrors("errorSamplesStoredLimit"),
25 | }) }}
26 |
27 | {{ forms.lightswitchField({
28 | label: "Automatically Trim Error Samples"|t("webperf"),
29 | instructions: "Whether the Error Samples should be trimmed after each new Error Sample is added"|t("webperf"),
30 | id: "automaticallyTrimErrorSamples",
31 | name: "automaticallyTrimErrorSamples",
32 | on: settings.automaticallyTrimErrorSamples,
33 | warning: configWarning("automaticallyTrimErrorSamples", "webperf"),
34 | errors: settings.getErrors("automaticallyTrimErrorSamples"),
35 | }) }}
36 | {% endnamespace %}
37 |
38 |
--------------------------------------------------------------------------------
/src/recommendations/CraftTwigTime.php:
--------------------------------------------------------------------------------
1 | sample->craftTwigMs >= self::MAX_TWIG_TIME) {
38 | $this->hasRecommendation = true;
39 | $this->summary = Craft::t(
40 | 'webperf',
41 | 'Look into decreasing the Twig template rendering time',
42 | []
43 | );
44 | $this->detail = Craft::t(
45 | 'webperf',
46 | 'The Twig templates took {displayCraftTwigMs} to render. Try to simplify the templates by doing less computation in Twig, and profiling them to see where the bottlenecks are.',
47 | [
48 | 'displayCraftTwigMs' => $this->displayMs($this->sample->craftTwigMs),
49 | ]
50 | );
51 | $this->learnMoreUrl = 'https://nystudio107.com/blog/profiling-your-website-with-craft-cms-3s-debug-toolbar';
52 |
53 | return;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nystudio107/craft-webperf",
3 | "description": "Webperf helps you build & maintain high quality websites through Real User Measurement of your website's performance",
4 | "type": "craft-plugin",
5 | "version": "5.0.0",
6 | "minimum-stability": "dev",
7 | "prefer-stable": true,
8 | "keywords": [
9 | "craft",
10 | "cms",
11 | "craftcms",
12 | "craft-plugin",
13 | "webperf"
14 | ],
15 | "support": {
16 | "docs": "https://nystudio107.com/docs/webperf",
17 | "issues": "https://nystudio107.com/plugins/webperf/support",
18 | "source": "https://github.com/nystudio107/craft-webperf"
19 | },
20 | "license": "proprietary",
21 | "authors": [
22 | {
23 | "name": "nystudio107",
24 | "homepage": "https://nystudio107.com"
25 | }
26 | ],
27 | "require": {
28 | "php": "^8.2",
29 | "craftcms/cms": "^5.0.0",
30 | "nystudio107/craft-plugin-vite": "^5.0.0",
31 | "jaybizzle/crawler-detect": "^1.2.37",
32 | "league/csv": "^8.2 || ^9.0",
33 | "whichbrowser/parser": "^2.0.37"
34 | },
35 | "require-dev": {
36 | "craftcms/ecs": "dev-main",
37 | "craftcms/phpstan": "dev-main",
38 | "craftcms/rector": "dev-main",
39 | "nystudio107/craft-minify": "^5.0.0",
40 | "nystudio107/craft-seomatic": "^5.0.0",
41 | "putyourlightson/craft-blitz": "^5.0.0"
42 | },
43 | "scripts": {
44 | "phpstan": "phpstan --ansi --memory-limit=2G",
45 | "check-cs": "ecs check --ansi",
46 | "fix-cs": "ecs check --fix --ansi"
47 | },
48 | "autoload": {
49 | "psr-4": {
50 | "nystudio107\\webperf\\": "src/"
51 | }
52 | },
53 | "config": {
54 | "allow-plugins": {
55 | "craftcms/plugin-installer": true,
56 | "yiisoft/yii2-composer": true
57 | },
58 | "optimize-autoloader": true,
59 | "sort-packages": true
60 | },
61 | "extra": {
62 | "class": "nystudio107\\webperf\\Webperf",
63 | "handle": "webperf",
64 | "name": "Webperf"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/recommendations/DomInteractive.php:
--------------------------------------------------------------------------------
1 | sample->domInteractive >= self::MAX_DOM_INTERACTIVE_TIME) {
38 | $this->hasRecommendation = true;
39 | $this->summary = Craft::t(
40 | 'webperf',
41 | 'The time before the user can interact with the page is too long',
42 | []
43 | );
44 | $this->detail = Craft::t(
45 | 'webperf',
46 | 'The time to interactive was {displayDomInteractive}. Try to reduce this by reducing the amount of JavaScript that is executed on your page, and ensure that [Marketing Tags](https://nystudio107.com/blog/tags-gone-wild) are kept in check.',
47 | [
48 | 'displayDomInteractive' => $this->displayMs($this->sample->domInteractive),
49 | ]
50 | );
51 | $this->learnMoreUrl = 'https://developers.google.com/web/tools/lighthouse/audits/time-to-interactive';
52 |
53 | return;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/RecommendationsList-T8Et27n2.js:
--------------------------------------------------------------------------------
1 | import{n as i,A as r}from"./vue-apexcharts-C2g27_eS.js";import{S as o}from"./SamplePaneFooter-DUo1brgB.js";const l=s=>({baseURL:s,headers:{"X-Requested-With":"XMLHttpRequest"}}),d=(s,e,t,a)=>{s.get(e,{params:t}).then(n=>{a&&a(n.data)}).catch(n=>{console.log(n)})},p={components:{"sample-pane-footer":o},props:{start:{type:String,default:""},end:{type:String,default:""},devModeWarning:{type:Boolean,default:!1},pageUrl:{type:String,default:""},siteId:{type:Number,default:0},apiUrl:{type:String,default:""}},data:function(){return{series:[],displayStart:this.start,displayEnd:this.end}},created(){this.getSeriesData()},mounted(){this.$events.$on("change-range",s=>this.onChangeRange(s))},methods:{getSeriesData:async function(){const s=r.create(l(this.apiUrl));let e={start:this.displayStart,end:this.displayEnd,pageUrl:this.pageUrl,siteId:this.siteId};await d(s,"",e,t=>{t[0]!==void 0&&(this.series=t)})},onChangeRange(s){this.displayStart=s.start,this.displayEnd=s.end,this.getSeriesData()}}};var c=function(){var e=this,t=e._self._c;return t("div",[e.series.length?e._e():t("div",{staticClass:"text-3xl text-center py-10"},[e._v(" 🎉 No recommendations found. Nice job! ")]),e._l(e.series,function(a,n){return t("div",{key:n},[t("div",{staticClass:"field pb-4"},[t("p",{staticClass:"warning text-2xl leading-normal"},[t("span",{domProps:{innerHTML:e._s(a.summary)}})]),t("div",{staticClass:"heading",staticStyle:{"padding-left":"26px"}},[t("p",{staticClass:"instructions text-xl leading-tight"},[t("span",{domProps:{innerHTML:e._s(a.detail)}}),t("span",{staticClass:"field inline-block m-0"},[a.learnMoreUrl!==""?t("a",{staticClass:"go notice",attrs:{href:a.learnMoreUrl,rel:"noopener,nofollow",target:"_blank"}},[e._v("Learn More")]):e._e()])])])])])}),t("sample-pane-footer",{attrs:{"display-dev-mode-warning":e.devModeWarning,"page-url":e.pageUrl,"site-id":e.siteId,column:"id",end:"end",start:"start",subject:"recommendations"}})],2)},g=[],u=i(p,c,g,!1,null,null,null,null);const h=u.exports;export{h as R};
2 | //# sourceMappingURL=RecommendationsList-T8Et27n2.js.map
3 |
--------------------------------------------------------------------------------
/src/base/CraftDataSampleTrait.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
11 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/web/assets/dist/img/Webperf-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
11 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/models/BoomerangDbDataSample.php:
--------------------------------------------------------------------------------
1 | dbRules(),
44 | [
45 | ['countryCode', DbStringValidator::class, 'max' => 2],
46 | ['device', DbStringValidator::class, 'max' => 50],
47 | ['browser', DbStringValidator::class, 'max' => 50],
48 | ['os', DbStringValidator::class, 'max' => 50],
49 | ]
50 | );
51 | }
52 |
53 | /**
54 | * @return array
55 | */
56 | public function behaviors(): array
57 | {
58 | return array_merge(
59 | parent::behaviors(),
60 | [
61 | 'typecast' => [
62 | 'class' => AttributeTypecastBehavior::class,
63 | // 'attributeTypes' will be composed automatically according to `rules()`
64 | ],
65 | ]
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/recommendations/FirstByte.php:
--------------------------------------------------------------------------------
1 | sample->firstByte >= self::MAX_TOTAL_TIME) {
38 | $this->hasRecommendation = true;
39 | $this->summary = Craft::t(
40 | 'webperf',
41 | 'The Time To First Byte (TTFB) is high',
42 | []
43 | );
44 | $this->detail = Craft::t(
45 | 'webperf',
46 | 'The time it took for the client to receive the first byte of data from the server was {displayFirstByte}. Look into decreasing that via the `{% cache %}` tag or some kind of static caching such as the [Blitz](https://github.com/putyourlightson/craft-blitz) plugin, [FastCGI Cache](https://nystudio107.com/blog/static-caching-with-craft-cms), or [Varnish](https://supercool.github.io/2015/06/08/making-craft-sing-with-varnish-and-nginx.html).',
47 | [
48 | 'displayFirstByte' => $this->displayMs($this->sample->firstByte),
49 | ]
50 | );
51 | $this->learnMoreUrl = 'https://craftcms.com/guides/why-is-my-site-slow';
52 |
53 | return;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://scrutinizer-ci.com/g/nystudio107/craft-webperf/?branch=v5) [](https://scrutinizer-ci.com/g/nystudio107/craft-webperf/?branch=v5) [](https://scrutinizer-ci.com/g/nystudio107/craft-webperf/build-status/v5) [](https://scrutinizer-ci.com/code-intelligence)
2 |
3 | # Webperf plugin for Craft CMS 5.x
4 |
5 | Webperf helps you build & maintain high quality websites through Real User Measurement of your website's performance
6 |
7 | 
8 |
9 | **Note**: _The license fee for this plugin is $59.00 via the Craft Plugin Store._
10 |
11 | ## Requirements
12 |
13 | This plugin requires Craft CMS 5.0.0 or later.
14 |
15 | ## Installation
16 |
17 | To install the plugin, follow these instructions.
18 |
19 | 1. Open your terminal and go to your Craft project:
20 |
21 | cd /path/to/project
22 |
23 | 2. Then tell Composer to load the plugin:
24 |
25 | composer require nystudio107/craft-webperf
26 |
27 | 3. Install the plugin via `./craft install/plugin webperf` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Webperf.
28 |
29 | You can also install Webperf via the **Plugin Store** in the Craft Control Panel.
30 |
31 | ## Documentation
32 |
33 | Click here -> [Webperf Documentation](https://nystudio107.com/plugins/webperf/documentation)
34 |
35 | ## Webperf Roadmap
36 |
37 | Some things to do, and ideas for potential features:
38 |
39 | * User definable "Alerts" that can be consumed by the Webhooks plugin
40 | * Have the Recommendations be a type of "Alert"
41 | * Comparison of multiple periods side by side to show comparative performance
42 |
43 | Brought to you by [nystudio107](https://nystudio107.com)
44 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/ErrorsDetailAreaChart-CoSEsohF.js:
--------------------------------------------------------------------------------
1 | import{n as o,a as h,A as p}from"./vue-apexcharts-C2g27_eS.js";const n=t=>t.map(function(s){return Math.max.apply(null,s)}),d=t=>({baseURL:t,headers:{"X-Requested-With":"XMLHttpRequest"}}),c=(t,s,e,r)=>{t.get(s,{params:e}).then(a=>{r&&r(a.data)}).catch(a=>{console.log(a)})},u={components:{apexcharts:h},props:{title:{type:String,default:""},start:{type:String,default:""},end:{type:String,default:""},pageUrl:{type:String,default:""},siteId:{type:Number,default:0},apiUrl:{type:String,default:""}},data:function(){return{chartOptions:{chart:{id:"vuechart-pages-detail",toolbar:{show:!1},sparkline:{enabled:!1},animations:{enabled:!1}},tooltip:{enabled:!0,inverseOrder:!0,x:{show:!1}},colors:["#1F9D55","#CC1F1A"],stroke:{curve:"smooth",width:3},fill:{type:"solid",opacity:.5,gradient:{enabled:!1}},legend:{formatter:void 0,offsetX:0,offsetY:-10},xaxis:{labels:{show:!1,minHeight:"20px"},crosshairs:{width:1}},yaxis:{min:0,max:0,seriesName:"Errors",tickAmount:1,labels:{formatter:t=>Math.round(t)}},labels:[],title:{text:this.title,offsetX:0,style:{fontSize:"24px",cssClass:"apexcharts-yaxis-title"}}},series:[{name:"empty",data:[0]}],displayStart:this.start,displayEnd:this.end,displayMaxValue:this.maxValue}},created(){this.getSeriesData()},mounted(){this.$events.$on("change-range",t=>this.onChangeRange(t))},methods:{getSeriesData:async function(){const t=p.create(d(this.apiUrl));let s={start:this.displayStart,end:this.displayEnd,pageUrl:this.pageUrl,siteId:this.siteId};await c(t,"",s,e=>{if(e[0]!==void 0){let r=n([e[0].data])[0],a=n([e[1].data])[0],i=r>a?r:a;this.chartOptions={...this.chartOptions,yaxis:{min:0,max:i,tickAmount:i>10?10:i,labels:{formatter:l=>Math.round(l)}},xaxis:{categories:e[0].labels,type:"category",labels:{show:!1,minHeight:"20px"},crosshairs:{width:1}},labels:e[0].labels},this.series=e}})},onChangeRange(t){this.displayStart=t.start,this.displayEnd=t.end,this.getSeriesData()}}};var f=function(){var s=this,e=s._self._c;return e("apexcharts",{attrs:{options:s.chartOptions,series:s.series,height:"450px",type:"area",width:"100%"}})},m=[],g=o(u,f,m,!1,null,null,null,null);const x=g.exports;export{x as E};
2 | //# sourceMappingURL=ErrorsDetailAreaChart-CoSEsohF.js.map
3 |
--------------------------------------------------------------------------------
/src/recommendations/FirstContentfulPaint.php:
--------------------------------------------------------------------------------
1 | sample->firstContentfulPaint >= self::MAX_FIRST_CONTENTFUL_PAINT_TIME) {
38 | $this->hasRecommendation = true;
39 | $this->summary = Craft::t(
40 | 'webperf',
41 | 'The wait is too long before content is displayed',
42 | []
43 | );
44 | $this->detail = Craft::t(
45 | 'webperf',
46 | 'The first contentful paint took {displayFirstContentfulPaint}. Try to avoid blocking the render by implementing [CriticalCSS](https://nystudio107.com/blog/implementing-critical-css), optimizing the [Critical Path](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/) by loading JavaScript asynchronously, and using the [font-display](https://css-tricks.com/font-display-masses/) property.',
47 | [
48 | 'displayFirstContentfulPaint' => $this->displayMs($this->sample->firstContentfulPaint),
49 | ]
50 | );
51 | $this->learnMoreUrl = 'https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint';
52 |
53 | return;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/controllers/RecommendationsController.php:
--------------------------------------------------------------------------------
1 | recommendations->data($pageUrl, $start, $end, $siteId);
61 | if (!empty($stats)) {
62 | $recSample = new RecommendationDataSample($stats);
63 | $data = Webperf::$plugin->recommendations->list($recSample);
64 | }
65 |
66 | return $this->asJson($data);
67 | }
68 |
69 | // Protected Methods
70 | // =========================================================================
71 | }
72 |
--------------------------------------------------------------------------------
/src/templates/settings/_includes/appearance.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 | {% from "webperf/_includes/macros.twig" import configWarning %}
3 |
4 | {% namespace "settings" %}
5 | {{ forms.lightswitchField({
6 | label: "Display Sidebar"|t("webperf"),
7 | instructions: "Whether the performance summary sidebar should be shown on entry, category, and product pages."|t("webperf"),
8 | id: "displaySidebar",
9 | name: "displaySidebar",
10 | on: settings.displaySidebar,
11 | warning: configWarning("displaySidebar", "webperf"),
12 | errors: settings.getErrors("displaySidebar"),
13 | }) }}
14 |
15 | {{ forms.colorField({
16 | label: "Dashboard Fast Color"|t("webperf"),
17 | instructions: "The color used to indicate fast timings on the Webperf Dashboard"|t("webperf"),
18 | id: "dashboardFastColor",
19 | name: "dashboardFastColor",
20 | value: settings.dashboardFastColor,
21 | warning: configWarning("dashboardFastColor", "webperf"),
22 | errors: settings.getErrors("dashboardFastColor"),
23 | }) }}
24 |
25 | {{ forms.colorField({
26 | label: "Dashboard Average Color"|t("webperf"),
27 | instructions: "The color used to indicate average timings on the Webperf Dashboard"|t("webperf"),
28 | id: "dashboardAverageColor",
29 | name: "dashboardAverageColor",
30 | value: settings.dashboardAverageColor,
31 | warning: configWarning("dashboardAverageColor", "webperf"),
32 | errors: settings.getErrors("dashboardAverageColor"),
33 | }) }}
34 |
35 | {{ forms.colorField({
36 | label: "Dashboard Slow Color"|t("webperf"),
37 | instructions: "The color used to indicate slow timings on the Webperf Dashboard"|t("webperf"),
38 | id: "dashboardSlowColor",
39 | name: "dashboardSlowColor",
40 | value: settings.dashboardSlowColor,
41 | warning: configWarning("dashboardSlowColor", "webperf"),
42 | errors: settings.getErrors("dashboardSlowColor"),
43 | }) }}
44 | {% endnamespace %}
45 |
46 |
--------------------------------------------------------------------------------
/src/base/DbErrorSampleTrait.php:
--------------------------------------------------------------------------------
1 | 120],
73 | ['url', DbStringValidator::class, 'max' => 255],
74 | ['queryString', DbStringValidator::class, 'max' => 255],
75 | ['type', DbStringValidator::class, 'max' => 16],
76 | ['pageErrors', ArrayValidator::class],
77 | [
78 | [
79 | 'title',
80 | 'type',
81 | 'url',
82 | 'queryString',
83 | ],
84 | 'string',
85 | ],
86 | ];
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SamplePaneFooter-DUo1brgB.js:
--------------------------------------------------------------------------------
1 | import{n as i,A as r}from"./vue-apexcharts-C2g27_eS.js";const l=t=>({baseURL:t,headers:{"X-Requested-With":"XMLHttpRequest"}}),o=(t,e,a,n)=>{t.get(e,{params:a}).then(s=>{n&&n(s.data)}).catch(s=>{console.log(s)})},d={components:{},props:{start:{type:String,default:""},end:{type:String,default:""},column:{type:String,default:""},displayDevModeWarning:{type:Boolean,default:!1},pageUrl:{type:String,default:""},subject:{type:String,default:""},siteId:{type:Number,default:0},apiUrl:{type:String,default:""}},data:function(){return{samples:0,displayStart:this.start,displayEnd:this.end,displayMaxValue:this.maxValue}},created(){this.getSeriesData()},mounted(){this.$events.$on("change-range",t=>this.onChangeRange(t))},methods:{getSeriesData:async function(){const t=r.create(l(this.apiUrl));let e={column:this.column,start:this.displayStart,end:this.displayEnd,pageUrl:this.pageUrl,siteId:this.siteId};await o(t,"",e,a=>{a.cnt!==void 0&&(this.samples=a.cnt)})},onChangeRange(t){this.displayStart=t.start,this.displayEnd=t.end,this.getSeriesData()},formatNumber(t){return t.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",")}}};var c=function(){var e=this,a=e._self._c;return a("div",{staticClass:"field"},[a("div",{staticClass:"heading"},[a("p",{staticClass:"instructions"},[e._v(" The "+e._s(e.subject)+" data is an "),a("em",[e._v("average")]),e._v(" of "),a("strong",[e._v(e._s(e.formatNumber(e.samples)))]),e._v(" data sample"),e.samples!==1?a("span",[e._v("s")]):e._e(),e._v(". ")])]),e.samples<100?a("p",{staticClass:"warning"},[e._v(" Webperf has collected less than "),a("strong",[e._v("100")]),e._v(" data samples. The sample size is not statistically significant, so above averaged results may not be meaningful. ")]):e._e(),e.displayDevModeWarning?a("p",{staticClass:"warning"},[e._v(" Craft performance will be slower than normal with "),a("code",[e._v("devMode")]),e._v(" enabled due to extensive logging and disabling of some caches. "),e._m(0)]):e._e()])},p=[function(){var t=this,e=t._self._c;return e("span",{staticClass:"field inline-block m-0"},[e("a",{staticClass:"notice go",attrs:{href:"https://craftcms.com/guides/what-dev-mode-does",target:"_blank"}},[t._v("Learn More")])])}],m=i(d,c,p,!1,null,null,null,null);const g=m.exports;export{g as S};
2 | //# sourceMappingURL=SamplePaneFooter-DUo1brgB.js.map
3 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/PerformanceDetailAreaChart-BW1ni5m3.js:
--------------------------------------------------------------------------------
1 | import{n as i,a as n,A as l}from"./vue-apexcharts-C2g27_eS.js";const o=t=>t.map(function(e){return Math.max.apply(null,e)}),h=t=>({baseURL:t,headers:{"X-Requested-With":"XMLHttpRequest"}}),p=(t,e,a,s)=>{t.get(e,{params:a}).then(r=>{s&&s(r.data)}).catch(r=>{console.log(r)})},d={components:{apexcharts:n},props:{title:{type:String,default:""},start:{type:String,default:""},end:{type:String,default:""},pageUrl:{type:String,default:""},siteId:{type:Number,default:0},apiUrl:{type:String,default:""}},data:function(){return{chartOptions:{chart:{id:"vuechart-pages-detail",toolbar:{show:!1},sparkline:{enabled:!1},animations:{enabled:!1}},dataLabels:{enabled:!1},tooltip:{enabled:!0,inverseOrder:!0,x:{show:!1}},colors:["#CC1F1A","#E3342F","#EF5753","#DE751F","#F6993F","#FAAD63","#2779BD","#3490DC","#6CB2EB","#BCDEFA"],stroke:{curve:"smooth",width:3},fill:{type:"solid",opacity:.9,gradient:{enabled:!1}},legend:{formatter:void 0,offsetX:0,offsetY:-10},xaxis:{type:"category",labels:{show:!1,minHeight:"20px"},crosshairs:{width:1}},yaxis:{min:0,max:0,seriesName:"Time",labels:{formatter:t=>this.statFormatter(t)}},labels:[],title:{text:this.title,offsetX:0,style:{fontSize:"24px",cssClass:"apexcharts-yaxis-title"}}},series:[{name:"empty",data:[0]}],displayStart:this.start,displayEnd:this.end,displayMaxValue:this.maxValue}},created(){this.getSeriesData()},mounted(){this.$events.$on("change-range",t=>this.onChangeRange(t))},methods:{getSeriesData:async function(){const t=l.create(h(this.apiUrl));let e={start:this.displayStart,end:this.displayEnd,pageUrl:this.pageUrl,siteId:this.siteId};await p(t,"",e,a=>{if(a[0]!==void 0){let s=o([a[9].data])[0];s=Math.ceil(s/1e3)*1e3,this.chartOptions={...this.chartOptions,yaxis:{min:0,max:s,labels:{formatter:r=>this.statFormatter(r)}},xaxis:{categories:a[0].labels,type:"category",labels:{show:!1,minHeight:"20px"},crosshairs:{width:1}},labels:a[0].labels},this.series=a}})},onChangeRange(t){this.displayStart=t.start,this.displayEnd=t.end,this.getSeriesData()},statFormatter(t){return Number(t/1e3).toFixed(2)+"s"}}};var c=function(){var e=this,a=e._self._c;return a("apexcharts",{attrs:{options:e.chartOptions,series:e.series,height:"450px",type:"area",width:"100%"}})},f=[],u=i(d,c,f,!1,null,null,null,null);const g=u.exports;export{g as P};
2 | //# sourceMappingURL=PerformanceDetailAreaChart-BW1ni5m3.js.map
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © nystudio107
2 |
3 | Permission is hereby granted to any person obtaining a copy of this software
4 | (the “Software”) to use, copy, modify, merge, publish and/or distribute copies
5 | of the Software, and to permit persons to whom the Software is furnished to do
6 | so, subject to the following conditions:
7 |
8 | 1. **Don’t plagiarize.** The above copyright notice and this license shall be
9 | included in all copies or substantial portions of the Software.
10 |
11 | 2. **Don’t use the same license on more than one project.** Each licensed copy
12 | of the Software shall be actively installed in no more than one production
13 | environment at a time.
14 |
15 | 3. **Don’t mess with the licensing features.** Software features related to
16 | licensing shall not be altered or circumvented in any way, including (but
17 | not limited to) license validation, payment prompts, feature restrictions,
18 | and update eligibility.
19 |
20 | 4. **Pay up.** Payment shall be made immediately upon receipt of any notice,
21 | prompt, reminder, or other message indicating that a payment is owed.
22 |
23 | 5. **Follow the law.** All use of the Software shall not violate any applicable
24 | law or regulation, nor infringe the rights of any other person or entity.
25 |
26 | Failure to comply with the foregoing conditions will automatically and
27 | immediately result in termination of the permission granted hereby. This
28 | license does not include any right to receive updates to the Software or
29 | technical support. Licensees bear all risk related to the quality and
30 | performance of the Software and any modifications made or obtained to it,
31 | including liability for actual and consequential harm, such as loss or
32 | corruption of data, and any necessary service, repair, or correction.
33 |
34 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
38 | LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN
39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41 |
--------------------------------------------------------------------------------
/src/console/controllers/SamplesController.php:
--------------------------------------------------------------------------------
1 | dataSamples->trimDataSamples($this->limit);
65 | echo Craft::t(
66 | 'webperf',
67 | 'Trimmed {rows} from webperf_data_samples table',
68 | ['rows' => $affectedRows]
69 | ) . PHP_EOL;
70 | echo Craft::t('webperf', 'Trimming error samples') . PHP_EOL;
71 | $affectedRows = Webperf::$plugin->errorSamples->trimErrorSamples($this->limit);
72 | echo Craft::t(
73 | 'webperf',
74 | 'Trimmed {rows} from webperf_error_samples table',
75 | ['rows' => $affectedRows]
76 | ) . PHP_EOL;
77 |
78 | return 0;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/records/Alerts.php:
--------------------------------------------------------------------------------
1 | [
87 | 'class' => AttributeTypecastBehavior::class,
88 | // 'attributeTypes' will be composed automatically according to `rules()`
89 | ],
90 | ]
91 | );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/templates/_frontend/scripts/capture-errors.twig:
--------------------------------------------------------------------------------
1 | // from https://github.com/akamai/boomerang/blob/master/tests/page-template-snippets/captureErrorsSnippetNoScript.tpl
2 | (function(w){
3 | w.BOOMR = w.BOOMR || {};
4 |
5 | w.BOOMR.globalOnErrorOrig = w.BOOMR.globalOnError = w.onerror;
6 | w.BOOMR.globalErrors = [];
7 |
8 | var now = (function() {
9 | try {
10 | if ("performance" in w) {
11 | return function() {
12 | return Math.round(w.performance.now() + performance.timing.navigationStart);
13 | };
14 | }
15 | }
16 | catch (ignore) {}
17 |
18 | return Date.now || function() {
19 | return new Date().getTime();
20 | };
21 | })();
22 |
23 | w.onerror = function BOOMR_plugins_errors_onerror(message, fileName, lineNumber, columnNumber, error) {
24 | if (w.BOOMR.version) {
25 | // If Boomerang has already loaded, the only reason this function would still be alive would be if
26 | // we're in the chain from another handler that overwrote window.onerror. In that case, we should
27 | // run globalOnErrorOrig which presumably hasn't been overwritten by Boomerang.
28 | if (typeof w.BOOMR.globalOnErrorOrig === "function") {
29 | w.BOOMR.globalOnErrorOrig.apply(w, arguments);
30 | }
31 |
32 | return;
33 | }
34 |
35 | if (typeof error !== "undefined" && error !== null) {
36 | error.timestamp = now();
37 | w.BOOMR.globalErrors.push(error);
38 | }
39 | else {
40 | w.BOOMR.globalErrors.push({
41 | message: message,
42 | fileName: fileName,
43 | lineNumber: lineNumber,
44 | columnNumber: columnNumber,
45 | noStack: true,
46 | timestamp: now()
47 | });
48 | }
49 |
50 | if (typeof w.BOOMR.globalOnError === "function") {
51 | w.BOOMR.globalOnError.apply(w, arguments);
52 | }
53 | };
54 |
55 | // make it easier to detect this is our wrapped handler
56 | w.onerror._bmr = true;
57 | })(window);
58 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/tri-color-blend-CUFlaG2k.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"tri-color-blend-CUFlaG2k.js","sources":["../../../../../buildchain/src/js/tri-color-blend.js"],"sourcesContent":["export default class TriColorBlend {\n\n constructor(clr1 = '#00C800', clr2 = '#FFFF00', clr3 = '#C80000')\n {\n this.clr1 = this.HexToRGB(clr1);\n this.clr2 = this.HexToRGB(clr2);\n this.clr3 = this.HexToRGB(clr3);\n }\n\n RGBToHex(r, g, b)\n {\n let bin = r << 16 | g << 8 | b;\n return (function (h) {\n return new Array(7 - h.length).join(\"0\") + h\n })(bin.toString(16).toUpperCase())\n }\n\n HexToRGB(hex)\n {\n let result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n return result ? {\n r: parseInt(result[1], 16),\n g: parseInt(result[2], 16),\n b: parseInt(result[3], 16)\n } : null;\n }\n\n colorFromPercentage(val)\n {\n let startColor = this.clr1;\n let endColor = this.clr2;\n if (val >= 50) {\n startColor = this.clr2;\n endColor = this.clr3;\n val = val - 50;\n }\n const multiplier = (val / 50);\n const r = Math.round(startColor.r + multiplier * (endColor.r - startColor.r));\n const g = Math.round(startColor.g + multiplier * (endColor.g - startColor.g));\n const b = Math.round(startColor.b + multiplier * (endColor.b - startColor.b));\n return '#' + this.RGBToHex(r,g,b);\n }\n}\n"],"names":["TriColorBlend","clr1","clr2","clr3","r","g","b","bin","h","hex","result","val","startColor","endColor","multiplier"],"mappings":"AAAe,MAAMA,CAAc,CAE/B,YAAYC,EAAO,UAAWC,EAAO,UAAWC,EAAO,UACvD,CACI,KAAK,KAAO,KAAK,SAASF,CAAI,EAC9B,KAAK,KAAO,KAAK,SAASC,CAAI,EAC9B,KAAK,KAAO,KAAK,SAASC,CAAI,CACjC,CAED,SAASC,EAAGC,EAAGC,EACf,CACI,IAAIC,EAAMH,GAAK,GAAKC,GAAK,EAAIC,EAC7B,OAAQ,SAAUE,EAAG,CACjB,OAAO,IAAI,MAAM,EAAIA,EAAE,MAAM,EAAE,KAAK,GAAG,EAAIA,CAC9C,EAAED,EAAI,SAAS,EAAE,EAAE,YAAW,CAAE,CACpC,CAED,SAASE,EACT,CACI,IAAIC,EAAS,4CAA4C,KAAKD,CAAG,EACjE,OAAOC,EAAS,CACZ,EAAG,SAASA,EAAO,CAAC,EAAG,EAAE,EACzB,EAAG,SAASA,EAAO,CAAC,EAAG,EAAE,EACzB,EAAG,SAASA,EAAO,CAAC,EAAG,EAAE,CAC5B,EAAG,IACP,CAED,oBAAoBC,EACpB,CACI,IAAIC,EAAa,KAAK,KAClBC,EAAW,KAAK,KAChBF,GAAO,KACPC,EAAa,KAAK,KAClBC,EAAW,KAAK,KAChBF,EAAMA,EAAM,IAEhB,MAAMG,EAAcH,EAAM,GACpBP,EAAI,KAAK,MAAMQ,EAAW,EAAIE,GAAcD,EAAS,EAAID,EAAW,EAAE,EACtEP,EAAI,KAAK,MAAMO,EAAW,EAAIE,GAAcD,EAAS,EAAID,EAAW,EAAE,EACtEN,EAAI,KAAK,MAAMM,EAAW,EAAIE,GAAcD,EAAS,EAAID,EAAW,EAAE,EAC5E,MAAO,IAAM,KAAK,SAASR,EAAEC,EAAEC,CAAC,CACnC,CACL"}
--------------------------------------------------------------------------------
/src/variables/WebperfVariable.php:
--------------------------------------------------------------------------------
1 | includeBeacon = $include;
37 | }
38 |
39 | /**
40 | * Whether to include the Craft beacon or not
41 | *
42 | * @param bool $include
43 | */
44 | public function includeCraftBeacon(bool $include): void
45 | {
46 | Webperf::$settings->includeCraftProfiling = $include;
47 | }
48 |
49 | /**
50 | * Change the type of render; either `html` or `amp-html` are valid for $renderType
51 | *
52 | * @param string $renderType
53 | */
54 | public function renderType(string $renderType): void
55 | {
56 | Webperf::$renderType = $renderType;
57 | }
58 |
59 | /**
60 | * @param int $siteId
61 | * @param string $column
62 | *
63 | * @return int|string
64 | */
65 | public function totalSamples(int $siteId, string $column): int|string
66 | {
67 | return Webperf::$plugin->dataSamples->totalSamples($siteId, $column);
68 | }
69 |
70 | /**
71 | * Get the total number of errors optionally limited by siteId, between
72 | * $start and $end
73 | *
74 | * @param int $siteId
75 | * @param string $start
76 | * @param string $end
77 | * @param string|null $pageUrl
78 | * @param string|null $type
79 | *
80 | * @return int
81 | */
82 | public function totalErrorSamplesRange(int $siteId, string $start, string $end, $pageUrl = null, $type = null): int
83 | {
84 | return Webperf::$plugin->errorSamples->totalErrorSamplesRange($siteId, $start, $end, $pageUrl, $type);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/templates/_includes/sites-menu.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 | {% set baseUrl = "webperf/#{controllerHandle}/" %}
4 | {% set params = params ?? [] %}
5 |
6 | {% if showSites %}
7 | {{ sitesMenuLabel }}
8 |
10 |
11 |
52 | {% endif %}
53 |
--------------------------------------------------------------------------------
/src/log/ErrorsTarget.php:
--------------------------------------------------------------------------------
1 | 'error',
29 | 2 => 'warning',
30 | ];
31 |
32 | // Public Properties
33 | // =========================================================================
34 |
35 | /**
36 | * @var array accumulated page errors
37 | */
38 | public $pageErrors = [];
39 |
40 | // Public Methods
41 | // =========================================================================
42 |
43 | /**
44 | * Processes the given log messages.
45 | * This method will filter the given messages with [[levels]] and
46 | * [[categories]]. And if requested, it will also export the filtering
47 | * result to specific medium (e.g. email).
48 | *
49 | * @param array $messages log messages to be processed. See
50 | * [[Logger::messages]] for the structure of each
51 | * message.
52 | * @param bool $final whether this method is called at the end of the
53 | * current application
54 | */
55 | public function collect($messages, $final)
56 | {
57 | // Merge in any messages intended for us
58 | $this->messages = array_merge(
59 | $this->messages,
60 | static::filterMessages($messages, $this->getLevels(), $this->categories, $this->except)
61 | );
62 | foreach ($this->messages as $message) {
63 | // Ignore objects/arrays
64 | if (!is_string($message[0])) {
65 | continue;
66 | }
67 | $this->pageErrors[] = [
68 | 'level' => self::ERROR_LEVELS[$message[1]],
69 | 'message' => $message[0],
70 | 'category' => $message[2],
71 | ];
72 | }
73 | $this->messages = [];
74 | if ($final) {
75 | $this->export();
76 | Webperf::$plugin->beacons->includeCraftErrorsBeacon();
77 | }
78 | }
79 |
80 | /**
81 | * @inheritdoc
82 | */
83 | public function export()
84 | {
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/widgets/Metrics.php:
--------------------------------------------------------------------------------
1 | 'Hello, world.'],
74 | ]
75 | );
76 | return $rules;
77 | }
78 |
79 | /**
80 | * @inheritdoc
81 | */
82 | public function getSettingsHtml(): ?string
83 | {
84 | return Craft::$app->getView()->renderTemplate(
85 | 'webperf/_components/widgets/Metrics_settings',
86 | [
87 | 'widget' => $this,
88 | ]
89 | );
90 | }
91 |
92 | /**
93 | * @inheritdoc
94 | */
95 | public function getBodyHtml(): ?string
96 | {
97 | return Craft::$app->getView()->renderTemplate(
98 | 'webperf/_components/widgets/Metrics_body',
99 | [
100 | 'message' => $this->message,
101 | ]
102 | );
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/base/BoomerangDataSampleTrait.php:
--------------------------------------------------------------------------------
1 |
4 | {% namespace "settings" %}
5 | {{ forms.textField({
6 | label: "Webperf Data Samples to Store"|t("webperf"),
7 | instructions: "How many unique Webperf Data Samples should be stored before they are trimmed."|t("webperf"),
8 | id: "dataSamplesStoredLimit",
9 | name: "dataSamplesStoredLimit",
10 | size: 10,
11 | maxlength: 10,
12 | value: settings.dataSamplesStoredLimit,
13 | warning: configWarning("dataSamplesStoredLimit", "webperf"),
14 | errors: settings.getErrors("dataSamplesStoredLimit"),
15 | }) }}
16 |
17 | {{ forms.lightswitchField({
18 | label: "Automatically Trim Data Samples"|t("webperf"),
19 | instructions: "Whether the Data Samples should be trimmed after each new Data Sample is added"|t("webperf"),
20 | id: "automaticallyTrimDataSamples",
21 | name: "automaticallyTrimDataSamples",
22 | on: settings.automaticallyTrimDataSamples,
23 | warning: configWarning("automaticallyTrimDataSamples", "webperf"),
24 | errors: settings.getErrors("automaticallyTrimDataSamples"),
25 | }) }}
26 |
27 | {{ forms.lightswitchField({
28 | label: "Trim Outlier Data Samples"|t("webperf"),
29 | instructions: "Whether outlier data samples that are `10x` the mean should be deleted"|t("webperf"),
30 | id: "trimOutlierDataSamples",
31 | name: "trimOutlierDataSamples",
32 | on: settings.trimOutlierDataSamples,
33 | warning: configWarning("trimOutlierDataSamples", "webperf"),
34 | errors: settings.getErrors("trimOutlierDataSamples"),
35 | }) }}
36 |
37 | {{ forms.selectField({
38 | label: "Sample Trimming Rate Limit"|t("webperf"),
39 | instructions: "The amount of time required between trimming of data samples."|t("webperf"),
40 | id: "samplesRateLimitMs",
41 | name: "samplesRateLimitMs",
42 | options: {
43 | 3600000: "Once per hour"|t("webperf"),
44 | 86400000: "Once per day"|t("webperf"),
45 | 604800000: "Once per week"|t("webperf"),
46 | },
47 | value: settings.samplesRateLimitMs,
48 | warning: configWarning("samplesRateLimitMs", "webperf"),
49 | errors: settings.getErrors("samplesRateLimitMs"),
50 | }) }}
51 |
52 | {{ forms.textField({
53 | label: "Rate Limit in ms"|t("webperf"),
54 | instructions: "The number of milliseconds required between recording of frontend beacon data samples."|t("webperf"),
55 | id: "rateLimitMs",
56 | name: "rateLimitMs",
57 | size: 6,
58 | maxlength: 5,
59 | value: settings.rateLimitMs,
60 | warning: configWarning("rateLimitMs", "webperf"),
61 | errors: settings.getErrors("rateLimitMs"),
62 | }) }}
63 | {% endnamespace %}
64 |
65 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/RequestBarChart-Ee0Vh_Iz.js:
--------------------------------------------------------------------------------
1 | import{n as r}from"./vue-apexcharts-C2g27_eS.js";const o={name:"RequestBarRecursive",props:{column:{type:String,default:""},color:{type:String,default:""},label:{type:String,default:""},value:{type:Number,default:0},parentValue:{type:Number,default:0},nodes:{type:Array,default:()=>[]}},methods:{statFormatter(a){return Number(a/1e3).toFixed(2)+"s"}}};var n=function(){var e=this,t=e._self._c;return t("div",{staticClass:"h-5",class:e.color,style:{width:e.value/e.parentValue*100+"%"},attrs:{title:e.label+" "+e.statFormatter(e.value)}},e._l(e.nodes,function(l){return t("request-bar-recursive",{key:l.column,attrs:{color:l.color,column:l.column,label:l.label,nodes:l.nodes,"parent-value":l.parentValue,value:l.value}})}),1)},s=[],u=r(o,n,s,!1,null,null,null,null);const c=u.exports,i=[{column:"pageLoad",color:"bg-blue-200",label:"Page Loaded"},{column:"domInteractive",color:"bg-blue-400",label:"DOM Interactive"},{column:"firstContentfulPaint",color:"bg-blue-500",label:"First Contentful Paint"},{column:"firstPaint",color:"bg-blue-700",label:"First Paint"},{column:"firstByte",color:"bg-orange-400",label:"First Byte"},{column:"connect",color:"bg-orange-500",label:"Connect"},{column:"dns",color:"bg-orange-700",label:"DNS Lookup"},{column:"craftTotalMs",color:"bg-red-400",label:"Craft Rendering"},{column:"craftTwigMs",color:"bg-red-500",label:"Twig Rendering"},{column:"craftDbMs",color:"bg-red-700",label:"Database Queries"}],d={name:"RequestBarChart",components:{"request-bar-recursive":c},props:{rowData:{type:Object,default:()=>({})}},data:function(){return{root:void 0}},mounted(){this.$events!==void 0&&this.$events.$on("refresh-table-components",a=>this.onTableRefresh(a))},created(){this.calculateNodes()},methods:{onTableRefresh:function(){this.calculateNodes()},statFormatter(a){return Number(a/1e3).toFixed(2)+"s"},calculateNodes:function(){this.root=void 0,i.forEach(a=>{let e={column:a.column,color:a.color,label:a.label,value:parseFloat(this.rowData[a.column])||null,parentValue:parseFloat(this.rowData.maxTotalPageLoad)||null,nodes:void 0};if(e.value)if(this.root){let t=this.root;for(;t;)!t.nodes||!t.value||e.value>t.value?(e.nodes=t.nodes,e.parentValue=t.parentValue||t.value,t.nodes=[e],t=e.nodes||void 0):t=t.nodes[0]||void 0}else this.root=e})}}};var f=function(){var e=this,t=e._self._c;return t("div",{staticClass:"flex flex-no-wrap"},[e.rowData.type==="both"?t("div",{staticClass:"flex-shrink",attrs:{title:"Combined Frontend & Craft Beacon"}},[t("div",{staticClass:"w-2 h-2 bg-blue-700 rounded-full mb-1"}),t("div",{staticClass:"w-2 h-2 bg-orange-700 rounded-full"})]):e._e(),e.rowData.type==="frontend"?t("div",{staticClass:"flex-shrink",attrs:{title:"Frontend Beacon only"}},[t("div",{staticClass:"w-2 h-2 bg-blue-700 rounded-full mb-1"}),t("div",{staticClass:"w-2 h-2 bg-transparent rounded-full"})]):e._e(),e.rowData.type==="craft"?t("div",{staticClass:"flex-shrink",attrs:{title:"Craft Beacon only"}},[t("div",{staticClass:"w-2 h-2 bg-transparent rounded-full mb-1"}),t("div",{staticClass:"w-2 h-2 bg-orange-700 rounded-full"})]):e._e(),t("div",{staticClass:"flex-grow"},[t("request-bar-recursive",{attrs:{color:e.root.color,column:e.root.column,label:e.root.label,nodes:e.root.nodes,"parent-value":e.root.parentValue,value:e.root.value}})],1),t("div",{staticClass:"flex-shrink"},[e._v(" "+e._s(e.statFormatter(e.root.value))+" ")])])},b=[],v=r(d,f,b,!1,null,null,null,null);const p=v.exports;export{p as R};
2 | //# sourceMappingURL=RequestBarChart-Ee0Vh_Iz.js.map
3 |
--------------------------------------------------------------------------------
/src/helpers/PluginTemplate.php:
--------------------------------------------------------------------------------
1 | getView()->renderString($templateString, $params);
38 | } catch (\Exception $e) {
39 | $html = Craft::t(
40 | 'webperf',
41 | 'Error rendering template string -> {error}',
42 | ['error' => $e->getMessage()]
43 | );
44 | Craft::error($html, __METHOD__);
45 | }
46 |
47 | return $html;
48 | }
49 |
50 | /**
51 | * Render a plugin template
52 | *
53 | * @param string $templatePath
54 | * @param array $params
55 | * @param string|null $minifier
56 | *
57 | * @return string
58 | */
59 | public static function renderPluginTemplate(
60 | string $templatePath,
61 | array $params = [],
62 | string $minifier = null,
63 | ): string {
64 | // Stash the old template mode, and set it Control Panel template mode
65 | $oldMode = Craft::$app->view->getTemplateMode();
66 | try {
67 | Craft::$app->view->setTemplateMode(View::TEMPLATE_MODE_CP);
68 | } catch (Exception $e) {
69 | Craft::error($e->getMessage(), __METHOD__);
70 | }
71 |
72 | // Render the template with our vars passed in
73 | try {
74 | $htmlText = Craft::$app->view->renderTemplate('webperf/' . $templatePath, $params);
75 | if ($minifier) {
76 | // If Minify is installed, minify the html
77 | /** @var Minify|null $minify */
78 | $minify = Craft::$app->getPlugins()->getPlugin(self::MINIFY_PLUGIN_HANDLE);
79 | if ($minify) {
80 | $htmlText = $minify->minify->$minifier($htmlText);
81 | }
82 | }
83 | } catch (\Exception $e) {
84 | $htmlText = Craft::t(
85 | 'webperf',
86 | 'Error rendering `{template}` -> {error}',
87 | ['template' => $templatePath, 'error' => $e->getMessage()]
88 | );
89 | Craft::error($htmlText, __METHOD__);
90 | }
91 |
92 | // Restore the old template mode
93 | try {
94 | Craft::$app->view->setTemplateMode($oldMode);
95 | } catch (Exception $e) {
96 | Craft::error($e->getMessage(), __METHOD__);
97 | }
98 |
99 | return Template::raw($htmlText);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/templates/errors/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Webperf plugin for Craft CMS 3.x
5 | *
6 | * Monitor the performance of your webpages through real-world user timing data
7 | *
8 | * @link https://nystudio107.com
9 | * @copyright Copyright (c) 2019 nystudio107
10 | */
11 | #}
12 |
13 | {% requirePermission "webperf:errors" %}
14 |
15 | {% extends "webperf/_layouts/webperf-cp.twig" %}
16 |
17 | {% import "_includes/forms" as forms %}
18 |
19 | {% block contextMenu %}
20 | {% include "webperf/_includes/sites-menu.twig" with {'params': {}} %}
21 | {% endblock %}
22 |
23 | {% block actionButton %}
24 |
38 | {% endblock %}
39 |
40 | {% block content %}
41 |
49 |
50 | {# -- Range Picker -- #}
51 | {% include "webperf/_includes/range-picker.twig" %}
52 |
53 |
54 | {{ "Aggregate Page Errors History"|t("webperf") }}
55 |
56 |
64 |
65 |
66 |
67 |
68 | {{ "Aggregate Page Errors"|t("webperf") }}
69 |
70 |
79 |
80 |
81 |
82 | {% endblock %}
83 |
84 | {% block foot %}
85 | {{ parent() }}
86 | {% set tagOptions = {
87 | 'depends': [
88 | 'nystudio107\\webperf\\assetbundles\\webperf\\WebperfAsset'
89 | ],
90 | } %}
91 | {{ craft.webperf.register('src/js/errors-index.js', false, tagOptions, tagOptions) }}
92 | {% endblock %}
93 |
--------------------------------------------------------------------------------
/src/templates/performance/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Webperf plugin for Craft CMS 3.x
5 | *
6 | * Monitor the performance of your webpages through real-world user timing data
7 | *
8 | * @link https://nystudio107.com
9 | * @copyright Copyright (c) 2019 nystudio107
10 | */
11 | #}
12 |
13 | {% requirePermission "webperf:performance" %}
14 |
15 | {% extends "webperf/_layouts/webperf-cp.twig" %}
16 |
17 | {% import "_includes/forms" as forms %}
18 |
19 | {% block contextMenu %}
20 | {% include "webperf/_includes/sites-menu.twig" with {'params': {}} %}
21 | {% endblock %}
22 |
23 | {% block actionButton %}
24 |
38 | {% endblock %}
39 |
40 | {% block content %}
41 |
49 |
50 | {# -- Range Picker -- #}
51 | {% include "webperf/_includes/range-picker.twig" %}
52 |
53 |
Aggregate Page Performance History
54 |
62 |
63 |
64 |
65 | {# -- Recommendations -- #}
66 | {% include "webperf/_includes/recommendations.twig" %}
67 |
68 |
69 |
Aggregate Page Performance Data Samples
70 |
80 |
81 |
82 |
83 | {% endblock %}
84 |
85 | {% block foot %}
86 | {{ parent() }}
87 | {% set tagOptions = {
88 | 'depends': [
89 | 'nystudio107\\webperf\\assetbundles\\webperf\\WebperfAsset'
90 | ],
91 | } %}
92 | {{ craft.webperf.register('src/js/performance-index.js', false, tagOptions, tagOptions) }}
93 | {% endblock %}
94 |
--------------------------------------------------------------------------------
/src/templates/settings/_includes/general.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 | {% from "webperf/_includes/macros.twig" import configWarning %}
3 |
4 | {% namespace "settings" %}
5 | {{ forms.textField({
6 | label: "Plugin name"|t("webperf"),
7 | instructions: "The public-facing name of the plugin"|t("webperf"),
8 | id: "pluginName",
9 | name: "pluginName",
10 | value: settings.pluginName,
11 | warning: configWarning("pluginName", "webperf"),
12 | errors: settings.getErrors("pluginName"),
13 | }) }}
14 |
15 | {{ forms.lightswitchField({
16 | label: "Include Browser Beacon"|t("webperf"),
17 | instructions: "Whether or not the asynchronous User Timing Beacon should be rendered on frontend pages."|t("webperf"),
18 | id: "includeBeacon",
19 | name: "includeBeacon",
20 | on: settings.includeBeacon,
21 | warning: configWarning("includeBeacon", "webperf"),
22 | errors: settings.getErrors("includeBeacon"),
23 | }) }}
24 |
25 | {{ forms.lightswitchField({
26 | label: "Include Craft Beacon"|t("webperf"),
27 | instructions: "Whether or not the Craft profiling data should be recorded for site requests."|t("webperf"),
28 | id: "includeCraftProfiling",
29 | name: "includeCraftProfiling",
30 | on: settings.includeCraftProfiling,
31 | warning: configWarning("includeCraftProfiling", "webperf"),
32 | errors: settings.getErrors("includeCraftProfiling"),
33 | }) }}
34 |
35 | {{ forms.lightswitchField({
36 | label: "Static Cached Site"|t("webperf"),
37 | instructions: "If the site is static cached, turn this option on to prevent Webperf from generating a unique beacon token."|t("webperf"),
38 | id: "staticCachedSite",
39 | name: "staticCachedSite",
40 | on: settings.staticCachedSite,
41 | warning: configWarning("staticCachedSite", "webperf"),
42 | errors: settings.getErrors("staticCachedSite"),
43 | }) }}
44 |
45 | {{ forms.autosuggestField({
46 | label: "WebPageTest.org API Key"|t("webperf"),
47 | instructions: "To run pages through [WebPageTest.org](https://www.webpagetest.org/) via an API, you need an API key. Enter your WebpageTest.org API key here. If you don't have one, they are free and easy to obtain. [Learn More](https://www.webpagetest.org/getkey.php)"|t("webperf"),
48 | suggestEnvVars: true,
49 | id: "webpageTestApiKey",
50 | name: "webpageTestApiKey",
51 | value: settings.webpageTestApiKey,
52 | warning: configWarning("webpageTestApiKey", "webperf"),
53 | errors: settings.getErrors("webpageTestApiKey"),
54 | }) }}
55 |
56 | {{ forms.editableTableField({
57 | label: "Exclude Patterns"|t("webperf"),
58 | instructions: "[Regular expressions](https://regexr.com/) to match URIs that should be excluded from tracking."|t("webperf"),
59 | id: 'excludePatterns',
60 | name: 'excludePatterns',
61 | required: false,
62 | defaultValues: {
63 | pattern: "",
64 | },
65 | cols: {
66 | pattern: {
67 | heading: "RegEx pattern to exclude"|t("webperf"),
68 | type: "singleline",
69 | width: "100%",
70 | code: true,
71 | },
72 | },
73 | rows: settings.excludePatterns,
74 | errors: settings.getErrors("excludePatterns"),
75 | }) }}
76 | {% endnamespace %}
77 |
78 |
--------------------------------------------------------------------------------
/src/helpers/Text.php:
--------------------------------------------------------------------------------
1 | truncate($length, $substring);
46 | }
47 |
48 | return $result;
49 | }
50 |
51 | /**
52 | * Truncates the string to a given length, while ensuring that it does not
53 | * split words. If $substring is provided, and truncating occurs, the
54 | * string is further truncated so that the substring may be appended without
55 | * exceeding the desired length.
56 | *
57 | * @param string $string The string to truncate
58 | * @param int $length Desired length of the truncated string
59 | * @param string $substring The substring to append if it can fit
60 | *
61 | * @return string with the resulting $str after truncating
62 | */
63 | public static function truncateOnWord($string, $length, $substring = '…'): string
64 | {
65 | $result = $string;
66 |
67 | if (!empty($string)) {
68 | $string = strip_tags($string);
69 | $result = (string)Stringy::create($string)->safeTruncate($length, $substring);
70 | }
71 |
72 | return $result;
73 | }
74 |
75 | /**
76 | * Clean up the passed in text by converting it to UTF-8, stripping tags,
77 | * removing whitespace, and decoding HTML entities
78 | *
79 | * @param string $text
80 | *
81 | * @return string
82 | */
83 | public static function cleanupText($text): string
84 | {
85 | if (empty($text)) {
86 | return '';
87 | }
88 | // Convert to UTF-8
89 | if (\function_exists('iconv')) {
90 | $text = iconv(mb_detect_encoding($text, mb_detect_order(), true), 'UTF-8//IGNORE', $text);
91 | } else {
92 | ini_set('mbstring.substitute_character', 'none');
93 | $text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');
94 | }
95 | // Strip HTML tags
96 | $text = strip_tags($text);
97 | // Remove excess whitespace
98 | $text = preg_replace('/\s{2,}/u', ' ', $text);
99 | // Decode any HTML entities
100 | $text = html_entity_decode($text);
101 |
102 | return $text;
103 | }
104 |
105 | // Protected Static Methods
106 | // =========================================================================
107 | }
108 |
--------------------------------------------------------------------------------
/src/templates/dashboard/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Webperf plugin for Craft CMS 3.x
5 | *
6 | * Monitor the performance of your webpages through real-world user timing data
7 | *
8 | * @link https://nystudio107.com
9 | * @copyright Copyright (c) 2019 nystudio107
10 | */
11 | #}
12 |
13 | {% requirePermission "webperf:dashboard" %}
14 |
15 | {% extends "webperf/_layouts/webperf-cp.twig" %}
16 |
17 | {% import "_includes/forms" as forms %}
18 |
19 | {% block contextMenu %}
20 | {% include "webperf/_includes/sites-menu.twig" with {'params': {}} %}
21 | {% endblock %}
22 |
23 | {% block actionButton %}
24 |
38 | {% endblock %}
39 |
40 | {% block content %}
41 |
46 |
47 | {% if showWelcome %}
48 |
49 |
50 |
51 |
52 |
54 |
Thanks for using Webperf!
55 |
56 | Monitor the performance of your webpages through Real User Measurement. Pass the RUM!
57 |
58 |
59 | Webperf has been installed, and will collect anonymous timing data as people visit your website.
60 |
61 |
We hope you love it! For more information, please see the documentation .
62 |
63 |
64 | {% endif %}
65 | {# -- Range Picker -- #}
66 | {% include "webperf/_includes/range-picker.twig" %}
67 | {% if settings.includeBeacon %}
68 | {# -- Frontend -- #}
69 | {% include "webperf/dashboard/_includes/frontend.twig" %}
70 |
71 | {# -- Backend -- #}
72 | {% include "webperf/dashboard/_includes/backend.twig" %}
73 | {% endif %}
74 |
75 | {% if settings.includeCraftProfiling %}
76 | {# -- Craft -- #}
77 | {% include "webperf/dashboard/_includes/craft.twig" %}
78 | {% endif %}
79 |
80 | {# -- Chart color key -- #}
81 | {% include "webperf/_includes/speed-color-key.twig" %}
82 |
83 | {# -- Recommendations -- #}
84 | {% include "webperf/_includes/recommendations.twig" %}
85 |
86 | {% endblock %}
87 |
88 | {% block foot %}
89 | {{ parent() }}
90 | {% set tagOptions = {
91 | 'depends': [
92 | 'nystudio107\\webperf\\assetbundles\\webperf\\WebperfAsset'
93 | ],
94 | } %}
95 | {{ craft.webperf.register('src/js/dashboard.js', false, tagOptions, tagOptions) }}
96 | {% endblock %}
97 |
--------------------------------------------------------------------------------
/src/services/ServicesTrait.php:
--------------------------------------------------------------------------------
1 | = 8.2, and config() is called before __construct(),
40 | // so we can't extract it from the passed in $config
41 | $majorVersion = '5';
42 | // Dev server container name & port are based on the major version of this plugin
43 | $devPort = 3000 + (int)$majorVersion;
44 | $versionName = 'v' . $majorVersion;
45 | return [
46 | 'components' => [
47 | 'beacons' => Beacons::class,
48 | 'dataSamples' => DataSamples::class,
49 | 'errorSamples' => ErrorSamples::class,
50 | 'recommendations' => Recommendations::class,
51 | // Register the vite service
52 | 'vite' => [
53 | 'assetClass' => WebperfAsset::class,
54 | 'checkDevServer' => true,
55 | 'class' => VitePluginService::class,
56 | 'devServerInternal' => 'http://craft-webperf-' . $versionName . '-buildchain-dev:' . $devPort,
57 | 'devServerPublic' => 'http://localhost:' . $devPort,
58 | 'errorEntry' => 'src/js/webperf.js',
59 | 'useDevServer' => true,
60 | ],
61 | ],
62 | ];
63 | }
64 |
65 | // Public Methods
66 | // =========================================================================
67 |
68 | /**
69 | * Returns the beacons service
70 | *
71 | * @return Beacons The beacons service
72 | * @throws InvalidConfigException
73 | */
74 | public function getBeacons(): Beacons
75 | {
76 | return $this->get('beacons');
77 | }
78 |
79 | /**
80 | * Returns the dataSamples service
81 | *
82 | * @return DataSamples The dataSamples service
83 | * @throws InvalidConfigException
84 | */
85 | public function getDataSamples(): DataSamples
86 | {
87 | return $this->get('dataSamples');
88 | }
89 |
90 | /**
91 | * Returns the errorSamples service
92 | *
93 | * @return ErrorSamples The errorSamples service
94 | * @throws InvalidConfigException
95 | */
96 | public function getErrorSamples(): ErrorSamples
97 | {
98 | return $this->get('errorSamples');
99 | }
100 |
101 | /**
102 | * Returns the recommendations service
103 | *
104 | * @return Recommendations The recommendations service
105 | * @throws InvalidConfigException
106 | */
107 | public function getRecommendations(): Recommendations
108 | {
109 | return $this->get('recommendations');
110 | }
111 |
112 | /**
113 | * Returns the vite service
114 | *
115 | * @return VitePluginService The vite service
116 | * @throws InvalidConfigException
117 | */
118 | public function getVite(): VitePluginService
119 | {
120 | return $this->get('vite');
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/controllers/DataSamplesController.php:
--------------------------------------------------------------------------------
1 | dataSamples->deleteSampleById($id)) {
45 | // Clear the caches and continue on
46 | Webperf::$plugin->clearAllCaches();
47 | Craft::$app->getSession()->setNotice(Craft::t('webperf', 'Data sample deleted.'));
48 |
49 | return $this->redirect(Craft::$app->getRequest()->referrer);
50 | }
51 | Craft::$app->getSession()->setError(Craft::t('webperf', "Couldn't delete data sample."));
52 |
53 | return $this->redirect(Craft::$app->getRequest()->referrer);
54 | }
55 |
56 | /**
57 | * @param string $pageUrl
58 | * @param int|null $siteId
59 | *
60 | * @return Response
61 | * @throws \craft\errors\MissingComponentException
62 | * @throws \yii\web\ForbiddenHttpException
63 | */
64 | public function actionDeleteSamplesByUrl(string $pageUrl, int $siteId = null): Response
65 | {
66 | // We may be passed 0 or other "empty" values, so coerce to null
67 | if (empty($siteId)) {
68 | $siteId = null;
69 | }
70 | PermissionHelper::controllerPermissionCheck('webperf:delete-data-samples');
71 | if (Webperf::$plugin->dataSamples->deleteDataSamplesByUrl($pageUrl, $siteId)) {
72 | // Clear the caches and continue on
73 | Webperf::$plugin->clearAllCaches();
74 | Craft::$app->getSession()->setNotice(Craft::t('webperf', 'Data samples deleted.'));
75 |
76 | return $this->redirect(Craft::$app->getRequest()->referrer);
77 | }
78 | Craft::$app->getSession()->setError(Craft::t('webperf', "Couldn't delete data samples."));
79 |
80 | return $this->redirect(Craft::$app->getRequest()->referrer);
81 | }
82 |
83 | /**
84 | * @param int|null $siteId
85 | *
86 | * @return Response
87 | * @throws \craft\errors\MissingComponentException
88 | * @throws \yii\web\ForbiddenHttpException
89 | */
90 | public function actionDeleteAllSamples(int $siteId = null): Response
91 | {
92 | // We may be passed 0 or other "empty" values, so coerce to null
93 | if (empty($siteId)) {
94 | $siteId = null;
95 | }
96 | PermissionHelper::controllerPermissionCheck('webperf:delete-data-samples');
97 | if (Webperf::$plugin->dataSamples->deleteAllDataSamples($siteId)) {
98 | // Clear the caches and continue on
99 | Webperf::$plugin->clearAllCaches();
100 | Craft::$app->getSession()->setNotice(Craft::t('webperf', 'All data samples deleted.'));
101 |
102 | return $this->redirect(Craft::$app->getRequest()->referrer);
103 | }
104 | Craft::$app->getSession()->setError(Craft::t('webperf', "Couldn't delete data samples."));
105 |
106 | return $this->redirect(Craft::$app->getRequest()->referrer);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/controllers/ErrorSamplesController.php:
--------------------------------------------------------------------------------
1 | errorSamples->deleteErrorSampleById($id)) {
45 | // Clear the caches and continue on
46 | Webperf::$plugin->clearAllCaches();
47 | Craft::$app->getSession()->setNotice(Craft::t('webperf', 'Error sample deleted.'));
48 |
49 | return $this->redirect(Craft::$app->getRequest()->referrer);
50 | }
51 | Craft::$app->getSession()->setError(Craft::t('webperf', "Couldn't delete error sample."));
52 |
53 | return $this->redirect(Craft::$app->getRequest()->referrer);
54 | }
55 |
56 | /**
57 | * @param string $pageUrl
58 | * @param int|null $siteId
59 | *
60 | * @return Response
61 | * @throws \craft\errors\MissingComponentException
62 | * @throws \yii\web\ForbiddenHttpException
63 | */
64 | public function actionDeleteSamplesByUrl(string $pageUrl, int $siteId = null): Response
65 | {
66 | // We may be passed 0 or other "empty" values, so coerce to null
67 | if (empty($siteId)) {
68 | $siteId = null;
69 | }
70 | PermissionHelper::controllerPermissionCheck('webperf:delete-error-samples');
71 | if (Webperf::$plugin->errorSamples->deleteErrorSamplesByUrl($pageUrl, $siteId)) {
72 | // Clear the caches and continue on
73 | Webperf::$plugin->clearAllCaches();
74 | Craft::$app->getSession()->setNotice(Craft::t('webperf', 'Error samples deleted.'));
75 |
76 | return $this->redirect(Craft::$app->getRequest()->referrer);
77 | }
78 | Craft::$app->getSession()->setError(Craft::t('webperf', "Couldn't delete error samples."));
79 |
80 | return $this->redirect(Craft::$app->getRequest()->referrer);
81 | }
82 |
83 | /**
84 | * @param int|null $siteId
85 | *
86 | * @return Response
87 | * @throws \craft\errors\MissingComponentException
88 | * @throws \yii\web\ForbiddenHttpException
89 | */
90 | public function actionDeleteAllSamples(int $siteId = null): Response
91 | {
92 | // We may be passed 0 or other "empty" values, so coerce to null
93 | if (empty($siteId)) {
94 | $siteId = null;
95 | }
96 | PermissionHelper::controllerPermissionCheck('webperf:delete-error-samples');
97 | if (Webperf::$plugin->errorSamples->deleteAllErrorSamples($siteId)) {
98 | // Clear the caches and continue on
99 | Webperf::$plugin->clearAllCaches();
100 | Craft::$app->getSession()->setNotice(Craft::t('webperf', 'All error samples deleted.'));
101 |
102 | return $this->redirect(Craft::$app->getRequest()->referrer);
103 | }
104 | Craft::$app->getSession()->setError(Craft::t('webperf', "Couldn't delete error samples."));
105 |
106 | return $this->redirect(Craft::$app->getRequest()->referrer);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SimpleBarChart-DDG34REw.js:
--------------------------------------------------------------------------------
1 | import{n as i,a as o,A as l}from"./vue-apexcharts-C2g27_eS.js";import{T as n}from"./tri-color-blend-CUFlaG2k.js";const d=t=>({baseURL:t,headers:{"X-Requested-With":"XMLHttpRequest"}}),p=(t,e,a,s)=>{t.get(e,{params:a}).then(r=>{s&&s(r.data)}).catch(r=>{console.log(r)})},h={components:{apexcharts:o},props:{title:{type:String,default:""},start:{type:String,default:""},end:{type:String,default:""},column:{type:String,default:""},pageUrl:{type:String,default:""},fastColor:{type:String,default:"#00C800"},averageColor:{type:String,default:"#FFFF00"},slowColor:{type:String,default:"#C80000"},maxValue:{type:Number,default:1e4},siteId:{type:Number,default:0},apiUrl:{type:String,default:""}},data:function(){return{chartOptions:{chart:{id:"vuechart-dashboard-radial-bar",fontFamily:"inherit",toolbar:{show:!1}},states:{hover:{filter:{type:"none",value:0}}},colors:["#000000"],plotOptions:{radialBar:{startAngle:-135,endAngle:135,hollow:{size:"65%"},track:{background:"#f1f5f8",strokeWidth:"97%",margin:5,shadow:{enabled:!0,top:2,left:0,color:"#999",opacity:1,blur:2}},dataLabels:{name:{show:!1,fontSize:"16px",color:"#333",offsetY:100},value:{offsetY:10,fontSize:"40px",color:"#333",style:{cssClass:"apexcharts-datalabel-value"},formatter:t=>(t=t*this.displayMaxValue/100,Number(t).toFixed(2)+"s")}}}},labels:[this.title],title:{text:this.title,offsetY:18,align:"center",style:{fontSize:"16px",cssClass:"apexcharts-title-text"}},stroke:{width:1,lineCap:"round"}},series:[0],displayStart:this.start,displayEnd:this.end,displayMaxValue:this.maxValue,triBlend:new n(this.fastColor,this.averageColor,this.slowColor)}},created(){this.getSeriesData()},mounted(){this.$events!==void 0&&this.$events.$on("change-range",t=>this.onChangeRange(t))},methods:{getSeriesData:async function(){const t=l.create(d(this.apiUrl));let e={column:this.column,start:this.displayStart,end:this.displayEnd,pageUrl:this.pageUrl,siteId:this.siteId};await p(t,"",e,a=>{if(a.avg!==void 0){let s=a.avg/1e3;s>this.displayMaxValue&&(this.displayMaxValue=s),s=s*100/this.displayMaxValue;let r=this.triBlend.colorFromPercentage(s);this.chartOptions={...this.chartOptions,colors:[r],plotOptions:{radialBar:{dataLabels:{value:{color:r}}}}},this.series=[s]}})},onChangeRange(t){this.displayStart=t.start,this.displayEnd=t.end,this.getSeriesData()}}};var u=function(){var e=this,a=e._self._c;return a("apexcharts",{attrs:{options:e.chartOptions,series:e.series,height:"300px",type:"radialBar",width:"100%"}})},c=[],f=i(h,u,c,!1,null,null,null,null);const b=f.exports,g=t=>({baseURL:t,headers:{"X-Requested-With":"XMLHttpRequest"}}),y=(t,e,a,s)=>{t.get(e,{params:a}).then(r=>{s&&s(r.data)}).catch(r=>{console.log(r)})},m={components:{},props:{title:{type:String,default:""},start:{type:String,default:""},end:{type:String,default:""},column:{type:String,default:""},pageUrl:{type:String,default:""},fastColor:{type:String,default:"#00C800"},averageColor:{type:String,default:"#FFFF00"},slowColor:{type:String,default:"#C80000"},maxValue:{type:Number,default:1e4},siteId:{type:Number,default:0},apiUrl:{type:String,default:""}},data:function(){return{barColor:"#000",series:[0],displayStart:this.start,displayEnd:this.end,displayMaxValue:this.maxValue,triBlend:new n(this.fastColor,this.averageColor,this.slowColor)}},created(){this.getSeriesData()},mounted(){this.$events!==void 0&&this.$events.$on("change-range",t=>this.onChangeRange(t))},methods:{getSeriesData:async function(){const t=l.create(g(this.apiUrl));let e={column:this.column,start:this.displayStart,end:this.displayEnd,pageUrl:this.pageUrl,siteId:this.siteId};await y(t,"",e,a=>{if(a.avg!==void 0){let s=a.avg/1e3;s>this.displayMaxValue&&(this.displayMaxValue=s),s=s*100/this.displayMaxValue,this.barColor=this.triBlend.colorFromPercentage(s),this.series=[s]}})},onChangeRange(t){this.displayStart=t.start,this.displayEnd=t.end,this.getSeriesData()},statFormatter(t){return t=t*this.displayMaxValue/100,Number(t).toFixed(2)+"s"}}};var C=function(){var e=this,a=e._self._c;return a("div",{staticClass:"simple-bar-chart-wrapper px-5 py-3"},[a("div",{staticClass:"clearafter py-2"},[a("div",{staticClass:"simple-bar-chart-label text-base font-bold"},[e._v(" "+e._s(e.title)+" ")]),a("div",{staticClass:"simple-bar-chart-value text-base font-bold"},[e._v(" "+e._s(e.statFormatter(e.series[0]))+" ")])]),a("div",{staticClass:"py-2"},[a("div",{staticClass:"simple-bar-chart-track rounded-full bg-gray-200"},[a("div",{staticClass:"simple-bar-line h-3 rounded-full",style:{width:e.series[0]+"%",backgroundColor:e.barColor}})])])])},v=[],S=i(m,C,v,!1,null,null,null,null);const F=S.exports;export{b as R,F as S};
2 | //# sourceMappingURL=SimpleBarChart-DDG34REw.js.map
3 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/SamplePaneFooter-DUo1brgB.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"SamplePaneFooter-DUo1brgB.js","sources":["../../../../../buildchain/src/vue/common/SamplePaneFooter.vue"],"sourcesContent":["\n \n
\n
\n The {{ subject }} data is an average of {{\n formatNumber(samples)\n }} data samples .\n
\n
\n
\n Webperf has collected less than 100 data samples. The\n sample size is not statistically significant, so above averaged results may not be meaningful.\n
\n
\n Craft performance will be slower than normal with\n devMode enabled due to extensive logging and disabling of some caches. Learn More \n
\n
\n \n\n\n"],"names":["configureApi","url","queryApi","api","uri","params","callback","result","error","_sfc_main","eventData","chartsAPI","Axios","data","range","number"],"mappings":"wDAoCA,MAAAA,EAAAC,IACA,CACA,QAAAA,EACA,QAAA,CACA,mBAAA,gBACA,CACA,GAGAC,EAAA,CAAAC,EAAAC,EAAAC,EAAAC,IAAA,CACAH,EAAA,IAAAC,EAAA,CAAA,OAAAC,CAAA,CAAA,EACA,KAAAE,GAAA,CACAD,GACAA,EAAAC,EAAA,IAAA,CAEA,CAAA,EACA,MAAAC,GAAA,CACA,QAAA,IAAAA,CAAA,CACA,CAAA,CACA,EAGAC,EAAA,CACA,WAAA,CAAA,EACA,MAAA,CACA,MAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,IAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,OAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,sBAAA,CACA,KAAA,QACA,QAAA,EACA,EACA,QAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,QAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,OAAA,CACA,KAAA,OACA,QAAA,CACA,EACA,OAAA,CACA,KAAA,OACA,QAAA,EACA,CACA,EACA,KAAA,UAAA,CACA,MAAA,CACA,QAAA,EACA,aAAA,KAAA,MACA,WAAA,KAAA,IACA,gBAAA,KAAA,QACA,CACA,EACA,SAAA,CACA,KAAA,cAAA,CACA,EACA,SAAA,CACA,KAAA,QAAA,IAAA,eAAAC,GAAA,KAAA,cAAAA,CAAA,CAAA,CACA,EACA,QAAA,CAEA,cAAA,gBAAA,CACA,MAAAC,EAAAC,EAAA,OAAAZ,EAAA,KAAA,MAAA,CAAA,EACA,IAAAK,EAAA,CACA,OAAA,KAAA,OACA,MAAA,KAAA,aACA,IAAA,KAAA,WACA,QAAA,KAAA,QACA,OAAA,KAAA,MACA,EACA,MAAAH,EAAAS,EAAA,GAAAN,EAAAQ,GAAA,CACAA,EAAA,MAAA,SACA,KAAA,QAAAA,EAAA,IAEA,CAAA,CACA,EACA,cAAAC,EAAA,CACA,KAAA,aAAAA,EAAA,MACA,KAAA,WAAAA,EAAA,IACA,KAAA,cAAA,CACA,EACA,aAAAC,EAAA,CACA,OAAAA,EAAA,SAAA,EAAA,QAAA,wBAAA,GAAA,CACA,CACA,CACA"}
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/RecommendationsList-T8Et27n2.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"RecommendationsList-T8Et27n2.js","sources":["../../../../../buildchain/src/vue/common/RecommendationsList.vue"],"sourcesContent":["\n \n
\n 🎉 No recommendations found. Nice job!\n
\n
\n
\n
\n \n\n\n"],"names":["configureApi","url","queryApi","api","uri","params","callback","result","error","_sfc_main","SamplePaneFooter","eventData","chartsAPI","Axios","data","range"],"mappings":"2GAwDA,MAAAA,EAAAC,IACA,CACA,QAAAA,EACA,QAAA,CACA,mBAAA,gBACA,CACA,GAGAC,EAAA,CAAAC,EAAAC,EAAAC,EAAAC,IAAA,CACAH,EAAA,IAAAC,EAAA,CAAA,OAAAC,CAAA,CAAA,EACA,KAAAE,GAAA,CACAD,GACAA,EAAAC,EAAA,IAAA,CAEA,CAAA,EACA,MAAAC,GAAA,CACA,QAAA,IAAAA,CAAA,CACA,CAAA,CACA,EAGAC,EAAA,CACA,WAAA,CACA,qBAAAC,CACA,EACA,MAAA,CACA,MAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,IAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,eAAA,CACA,KAAA,QACA,QAAA,EACA,EACA,QAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,OAAA,CACA,KAAA,OACA,QAAA,CACA,EACA,OAAA,CACA,KAAA,OACA,QAAA,EACA,CACA,EACA,KAAA,UAAA,CACA,MAAA,CACA,OAAA,CAAA,EACA,aAAA,KAAA,MACA,WAAA,KAAA,GACA,CACA,EACA,SAAA,CACA,KAAA,cAAA,CACA,EACA,SAAA,CACA,KAAA,QAAA,IAAA,eAAAC,GAAA,KAAA,cAAAA,CAAA,CAAA,CACA,EACA,QAAA,CAEA,cAAA,gBAAA,CACA,MAAAC,EAAAC,EAAA,OAAAb,EAAA,KAAA,MAAA,CAAA,EACA,IAAAK,EAAA,CACA,MAAA,KAAA,aACA,IAAA,KAAA,WACA,QAAA,KAAA,QACA,OAAA,KAAA,MACA,EACA,MAAAH,EAAAU,EAAA,GAAAP,EAAAS,GAAA,CACAA,EAAA,CAAA,IAAA,SACA,KAAA,OAAAA,EAEA,CAAA,CACA,EACA,cAAAC,EAAA,CACA,KAAA,aAAAA,EAAA,MACA,KAAA,WAAAA,EAAA,IACA,KAAA,cAAA,CACA,CACA,CACA"}
--------------------------------------------------------------------------------
/src/services/Recommendations.php:
--------------------------------------------------------------------------------
1 | $sample]);
65 | if ($rec->hasRecommendation) {
66 | $data[] = [
67 | 'summary' => Markdown::processParagraph($rec->summary),
68 | 'detail' => Markdown::processParagraph($rec->detail),
69 | 'learnMoreUrl' => $rec->learnMoreUrl,
70 | ];
71 | }
72 | }
73 | return $data;
74 | }
75 |
76 | /**
77 | * Return a list of recommendations
78 | *
79 | * @param string $pageUrl
80 | * @param string $start
81 | * @param string $end
82 | * @param int $siteId
83 | *
84 | * @return array
85 | */
86 | public function data(
87 | $pageUrl = '',
88 | string $start = '',
89 | string $end = '',
90 | $siteId = 0,
91 | ): array {
92 | $data = [];
93 | $db = Craft::$app->getDb();
94 | // Add a day since YYYY-MM-DD is really YYYY-MM-DD 00:00:00
95 | $end = date('Y-m-d', strtotime($end . '+1 day'));
96 | $pageUrl = urldecode($pageUrl);
97 | // Query the db table
98 | $query = (new Query())
99 | ->select([
100 | 'COUNT([[url]]) AS [[cnt]]',
101 |
102 | 'AVG([[pageLoad]]) AS [[pageLoad]]',
103 | 'AVG([[domInteractive]]) AS [[domInteractive]]',
104 | 'AVG([[firstContentfulPaint]]) AS [[firstContentfulPaint]]',
105 | 'AVG([[firstPaint]]) AS [[firstPaint]]',
106 | 'AVG([[firstByte]]) AS [[firstByte]]',
107 | 'AVG([[connect]]) AS [[connect]]',
108 | 'AVG([[dns]]) AS [[dns]]',
109 |
110 | 'AVG([[craftTotalMs]]) AS [[craftTotalMs]]',
111 | 'AVG([[craftDbMs]]) AS [[craftDbMs]]',
112 | 'AVG([[craftDbCnt]]) AS [[craftDbCnt]]',
113 | 'AVG([[craftTwigMs]]) AS [[craftTwigMs]]',
114 | 'AVG([[craftTwigCnt]]) AS [[craftTwigCnt]]',
115 | 'AVG([[craftOtherMs]]) AS [[craftOtherMs]]',
116 | 'AVG([[craftOtherCnt]]) AS [[craftOtherCnt]]',
117 | 'AVG([[craftTotalMemory]]) AS [[craftTotalMemory]]',
118 | ])
119 | ->from(['{{%webperf_data_samples}}'])
120 | ->where(['between', 'dateCreated', $start, $end]);
121 | if (!empty($pageUrl)) {
122 | $query->andWhere(['url' => $pageUrl]);
123 | }
124 | if ((int)$siteId !== 0) {
125 | $query->andWhere(['siteId' => $siteId]);
126 | }
127 | $stats = $query->all();
128 | if ($stats) {
129 | $data = $stats[0];
130 | }
131 |
132 | return $data;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/templates/errors/page-detail.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Webperf plugin for Craft CMS 3.x
5 | *
6 | * Monitor the performance of your webpages through real-world user timing data
7 | *
8 | * @link https://nystudio107.com
9 | * @copyright Copyright (c) 2019 nystudio107
10 | */
11 | #}
12 |
13 | {% requirePermission "webperf:errors-detail" %}
14 |
15 | {% extends "webperf/_layouts/webperf-cp.twig" %}
16 |
17 | {% import "_includes/forms" as forms %}
18 |
19 | {% block contextMenu %}
20 | {% include "webperf/_includes/sites-menu.twig" with {'params': {'pageUrl': pageUrl}} %}
21 | {% endblock %}
22 |
23 | {% block actionButton %}
24 |
38 | {% endblock %}
39 |
40 | {% block content %}
41 |
49 |
50 | {# -- Range Picker -- #}
51 | {% include "webperf/_includes/range-picker.twig" %}
52 |
53 |
54 | {% if pageTitle | length %}
55 |
64 | {% else %}
65 |
67 | {{ "Craft backend route"|t("webperf") }}
68 |
69 | {% endif %}
70 |
80 | {% set devModeWarning = 'false' %}
81 |
82 |
83 |
84 |
{{ "Page Error History"|t("webperf") }}
85 |
93 |
94 |
95 |
96 |
{{ "Page Errors Detail"|t("webperf") }}
97 |
104 |
105 |
106 |
107 | {% endblock %}
108 |
109 | {% block foot %}
110 | {{ parent() }}
111 | {% set tagOptions = {
112 | 'depends': [
113 | 'nystudio107\\webperf\\assetbundles\\webperf\\WebperfAsset'
114 | ],
115 | } %}
116 | {{ craft.webperf.register('src/js/errors-detail.js', false, tagOptions, tagOptions) }}
117 | {% endblock %}
118 |
--------------------------------------------------------------------------------
/src/controllers/SettingsController.php:
--------------------------------------------------------------------------------
1 | pluginName;
65 | $templateTitle = Craft::t('webperf', 'Settings');
66 | $view = Craft::$app->getView();
67 | // Asset bundle
68 | try {
69 | $view->registerAssetBundle(WebperfAsset::class);
70 | } catch (InvalidConfigException $e) {
71 | Craft::error($e->getMessage(), __METHOD__);
72 | }
73 | $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
74 | '@nystudio107/webperf/web/assets/dist',
75 | true
76 | );
77 | // Basic variables
78 | $variables['fullPageForm'] = true;
79 | $variables['docsUrl'] = self::DOCUMENTATION_URL;
80 | $variables['pluginName'] = $pluginName;
81 | $variables['title'] = $templateTitle;
82 | $variables['crumbs'] = [
83 | [
84 | 'label' => $pluginName,
85 | 'url' => UrlHelper::cpUrl('webperf'),
86 | ],
87 | [
88 | 'label' => $templateTitle,
89 | 'url' => UrlHelper::cpUrl('webperf/settings'),
90 | ],
91 | ];
92 | $variables['docTitle'] = "{$pluginName} - {$templateTitle}";
93 | $variables['selectedSubnavItem'] = 'settings';
94 | $variables['settings'] = $settings;
95 |
96 | // Render the template
97 | return $this->renderTemplate('webperf/settings', $variables);
98 | }
99 |
100 | /**
101 | * Saves a plugin’s settings.
102 | *
103 | * @return Response|null
104 | * @throws NotFoundHttpException if the requested plugin cannot be found
105 | * @throws BadRequestHttpException
106 | * @throws MissingComponentException
107 | * @throws ForbiddenHttpException
108 | */
109 | public function actionSavePluginSettings()
110 | {
111 | PermissionHelper::controllerPermissionCheck('webperf:settings');
112 | $this->requirePostRequest();
113 | $pluginHandle = Craft::$app->getRequest()->getRequiredBodyParam('pluginHandle');
114 | $settings = Craft::$app->getRequest()->getBodyParam('settings', []);
115 | $plugin = Craft::$app->getPlugins()->getPlugin($pluginHandle);
116 |
117 | if ($plugin === null) {
118 | throw new NotFoundHttpException('Plugin not found');
119 | }
120 |
121 | if (!Craft::$app->getPlugins()->savePluginSettings($plugin, $settings)) {
122 | Craft::$app->getSession()->setError(Craft::t('app', "Couldn't save plugin settings."));
123 |
124 | // Send the plugin back to the template
125 | /** @var UrlManager $urlManager */
126 | $urlManager = Craft::$app->getUrlManager();
127 | $urlManager->setRouteParams([
128 | 'plugin' => $plugin,
129 | ]);
130 |
131 | return null;
132 | }
133 |
134 | Webperf::$plugin->clearAllCaches();
135 | Craft::$app->getSession()->setNotice(Craft::t('app', 'Plugin settings saved.'));
136 |
137 | return $this->redirectToPostedUrl();
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/recommendations/MemoryLimit.php:
--------------------------------------------------------------------------------
1 | memoryLimit();
38 | $this->sample->craftTotalMemory = (int)$this->sample->craftTotalMemory;
39 | if ($phpMemoryLimit && $this->sample->craftTotalMemory) {
40 | $ratio = $phpMemoryLimit / $this->sample->craftTotalMemory;
41 | $displayCraftTotalMemory = (($this->sample->craftTotalMemory / 1024) / 1024) . 'M';
42 | $displayPhpMemoryLimit = (($phpMemoryLimit / 1024) / 1024) . 'M';
43 | $displayCraftMinMemory = ((self::MIN_CRAFT_MEMORY / 1024) / 1024) . 'M';
44 | $displayCraftMaxMemory = ((self::MAX_CRAFT_MEMORY / 1024) / 1024) . 'M';
45 | $this->summary = Craft::t(
46 | 'webperf',
47 | 'Check the `memory_limit` setting in your `php.ini` file',
48 | []
49 | );
50 | // See if they have enough memory allocated
51 | if ($phpMemoryLimit < self::MIN_CRAFT_MEMORY) {
52 | $this->hasRecommendation = true;
53 | $this->detail = Craft::t(
54 | 'webperf',
55 | 'Pixel & Tonic recommends at least {displayCraftMinMemory} allocated to PHP for Craft CMS 3. You have only {displayPhpMemoryLimit} allocated in your `php.ini` file.',
56 | [
57 | 'displayPhpMemoryLimit' => $displayPhpMemoryLimit,
58 | 'displayCraftMinMemory' => $displayCraftMinMemory,
59 | ]
60 | );
61 | $this->learnMoreUrl = 'https://docs.craftcms.com/v3/requirements.html';
62 |
63 | return;
64 | }
65 | // See if they have too much memory allocated
66 | if ($phpMemoryLimit >= self::MAX_CRAFT_MEMORY) {
67 | $this->hasRecommendation = true;
68 | $this->detail = Craft::t(
69 | 'webperf',
70 | 'Your `php.ini` file has `memory_limit` set to {displayPhpMemoryLimit}. This may be set too high, since it is a per-process memory limit, and memory-intensive image transforms are done in a process separate from PHP.',
71 | [
72 | 'displayPhpMemoryLimit' => $displayPhpMemoryLimit,
73 | ]
74 | );
75 | $this->learnMoreUrl = 'https://docs.craftcms.com/v3/requirements.html';
76 |
77 | return;
78 | }
79 | // See if they have too much memory allocated
80 | if ($ratio < 1.5) {
81 | $this->hasRecommendation = true;
82 | $this->detail = Craft::t(
83 | 'webperf',
84 | 'Your `php.ini` file has `memory_limit` set to {displayPhpMemoryLimit}, but Craft is using {displayCraftTotalMemory}. Consider raising your `memory_limit` to maintain a `1.5x` buffer of available memory.',
85 | [
86 | 'displayPhpMemoryLimit' => $displayPhpMemoryLimit,
87 | 'displayCraftTotalMemory' => $displayCraftTotalMemory,
88 | ]
89 | );
90 | $this->learnMoreUrl = 'https://docs.craftcms.com/v3/requirements.html';
91 |
92 | return;
93 | }
94 | }
95 | }
96 |
97 | // Protected Methods
98 | // =========================================================================
99 |
100 | /**
101 | * @return int
102 | */
103 | protected function memoryLimit(): int
104 | {
105 | $memoryLimit = ini_get('memory_limit');
106 | if (preg_match('/^(\d+)(.)$/', $memoryLimit, $matches)) {
107 | if (strtoupper($matches[2]) === 'G') {
108 | $memoryLimit = (int)$matches[1] * 1024 * 1024 * 1024; // nnnG -> nnn GB
109 | } elseif (strtoupper($matches[2]) === 'M') {
110 | $memoryLimit = (int)$matches[1] * 1024 * 1024; // nnnM -> nnn MB
111 | } elseif (strtoupper($matches[2]) === 'K') {
112 | $memoryLimit = (int)$matches[1] * 1024; // nnnK -> nnn KB
113 | }
114 | }
115 |
116 | return (int)$memoryLimit;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/controllers/FileController.php:
--------------------------------------------------------------------------------
1 | exportCsvFile(
89 | 'webperf-data-samples',
90 | '{{%webperf_data_samples}}',
91 | self::EXPORT_DATA_SAMPLES_COLUMNS,
92 | $pageUrl,
93 | $siteId
94 | );
95 | } catch (CannotInsertRecord $e) {
96 | Craft::error($e->getMessage(), __METHOD__);
97 | }
98 | }
99 |
100 | /**
101 | * Export the error samples table as a CSV file
102 | *
103 | * @param string $pageUrl
104 | * @param int|null $siteId
105 | *
106 | * @throws \yii\web\ForbiddenHttpException
107 | */
108 | public function actionExportErrorSamples(string $pageUrl = '', int $siteId = null)
109 | {
110 | PermissionHelper::controllerPermissionCheck('webperf:errors');
111 | try {
112 | $this->exportCsvFile(
113 | 'webperf-error-samples',
114 | '{{%webperf_error_samples}}',
115 | self::EXPORT_ERROR_SAMPLES_COLUMNS,
116 | $pageUrl,
117 | $siteId
118 | );
119 | } catch (CannotInsertRecord $e) {
120 | Craft::error($e->getMessage(), __METHOD__);
121 | }
122 | }
123 |
124 | // Public Methods
125 | // =========================================================================
126 |
127 | /**
128 | * @param string $filename
129 | * @param string $table
130 | * @param array $columns
131 | * @param string $pageUrl
132 | * @param int|null $siteId
133 | *
134 | * @throws \League\Csv\CannotInsertRecord
135 | */
136 | protected function exportCsvFile(
137 | string $filename,
138 | string $table,
139 | array $columns,
140 | string $pageUrl = '',
141 | int $siteId = null,
142 | ) {
143 | // If your CSV document was created or is read on a Macintosh computer,
144 | // add the following lines before using the library to help PHP detect line ending in Mac OS X
145 | if (!ini_get('auto_detect_line_endings')) {
146 | ini_set('auto_detect_line_endings', '1');
147 | }
148 | // Query the db table
149 | $query = (new Query())
150 | ->from([$table])
151 | ->select($columns)
152 | ;
153 | if (!empty($siteId)) {
154 | $query
155 | ->where(['siteId' => $siteId])
156 | ;
157 | }
158 | if (!empty($pageUrl)) {
159 | $query
160 | ->where(['pageUrl' => $pageUrl])
161 | ;
162 | }
163 | $data = $query
164 | ->all();
165 | // Create our CSV file writer
166 | $csv = Writer::createFromFileObject(new \SplTempFileObject());
167 | $csv->insertOne($columns);
168 | $csv->insertAll($data);
169 | $csv->output($filename . '.csv');
170 | exit(0);
171 | }
172 | }
173 |
--------------------------------------------------------------------------------