Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Giehl <stefan@matomo.org>2020-04-17 16:00:51 +0300
committerGitHub <noreply@github.com>2020-04-17 16:00:51 +0300
commit6936b93cba5150e0eaa879aebf3662b3e279d045 (patch)
treec63c93f92bc31479aa70d463939ef144eeb83e66 /plugins/PagePerformance
parent3c50481031e8b8ad647d8a74802e9558cebe41ce (diff)
New page performance reports (#15736)
* Adds various new performance metrics and dimensions * Adds tracking for new performance values * track performance metrics only for page views * Archive new performance metrics * move everything into a new plugin * fix archiving of overall metrics * Adds new overview reports * show performance metric on some more reports * adds new page performance icon * Adds new row action to view page performance evolution for pages * Adds new stacked bar visualization for page performance evolutions * show total value in stacked bar chart tooltips * [TEMP] use php tracker package branch * Adds some simple System tests * Adds some UI tests * remove performance metrics from action reports that don't support it * move calculation to api * mark as tracker plugin * improve calculation of maximum value in bar evolution chart * enrich existing tests with performance metrics * updates expected test files * send performance metrics with the next request after they are available this might not be the pageview it self but any request after it, like a ping, goal, ... * Adds request processor to process performance metrics not sent directly with the page view * rebuilt js * Add metric decriptions to evolution chart documentation * fix convertion of microseconds part * Ensure average page load time is displayed in evolution graph in scheduled reports * fix some more tests * move page performance overview to visitors overview * Adds new table with performance metrics visualization * Adds some additional information to page performance evolution overlay * update omnifixture * updates expected UI files * Use mediumints for new dimensions * Adds additional permission check * Encode label in page performance overlay title * Improve updating performance metrics in later requests * Adds some integration tests * improves metric documentations * Send already available performance data with page view request * update tests * updates expected UI test screenshots * updates expected test files * improves archiving * show page generation time in performance metrics table if matomo was installed before 4.0 * Hide page generation time in ui reports if Matomo was installed after 4.0 * Fix removal of unavailable columns from being displayed that was done too early in the process causing to be overwritten again by the reports configureView * do not track automatically calculated generation time any more * split latency into network and server time * [TEMP] update php tracker * rebuilt piwik.js * Ensure to count zero values as hits * updates Omnifixture * updates expected test files * remove possibility to set generation time * rebuilt piwik.js * adjust tests * update php tracker * update test logs * submodule * update Omnifixture * show page load time in action tooltip and visitor summary instead of generation time * updates expected ui files * mark page generation time metric as deprecated * fix tests * [TEMP] use submodule branches * ensure lower metric values are shown as better * use 4.x-dev branch of php-tracker * update submodules
Diffstat (limited to 'plugins/PagePerformance')
-rw-r--r--plugins/PagePerformance/API.php127
-rw-r--r--plugins/PagePerformance/Archiver.php124
-rw-r--r--plugins/PagePerformance/Columns/Metrics/AveragePageLoadTime.php63
-rw-r--r--plugins/PagePerformance/Columns/Metrics/AveragePerformanceMetric.php99
-rw-r--r--plugins/PagePerformance/Columns/Metrics/AverageTimeDomCompletion.php34
-rw-r--r--plugins/PagePerformance/Columns/Metrics/AverageTimeDomProcessing.php33
-rw-r--r--plugins/PagePerformance/Columns/Metrics/AverageTimeNetwork.php33
-rw-r--r--plugins/PagePerformance/Columns/Metrics/AverageTimeOnLoad.php33
-rw-r--r--plugins/PagePerformance/Columns/Metrics/AverageTimeServer.php33
-rw-r--r--plugins/PagePerformance/Columns/Metrics/AverageTimeTransfer.php33
-rw-r--r--plugins/PagePerformance/Columns/TimeDomCompletion.php74
-rw-r--r--plugins/PagePerformance/Columns/TimeDomProcessing.php74
-rw-r--r--plugins/PagePerformance/Columns/TimeNetwork.php74
-rw-r--r--plugins/PagePerformance/Columns/TimeOnLoad.php74
-rw-r--r--plugins/PagePerformance/Columns/TimeServer.php74
-rw-r--r--plugins/PagePerformance/Columns/TimeTransfer.php74
-rw-r--r--plugins/PagePerformance/Controller.php157
-rw-r--r--plugins/PagePerformance/JqplotDataGenerator/Chart.php139
-rw-r--r--plugins/PagePerformance/JqplotDataGenerator/StackedBarEvolution.php110
-rw-r--r--plugins/PagePerformance/Metrics.php131
-rw-r--r--plugins/PagePerformance/PagePerformance.php157
-rw-r--r--plugins/PagePerformance/Reports/Get.php90
-rw-r--r--plugins/PagePerformance/Tracker/PerformanceDataProcessor.php108
-rw-r--r--plugins/PagePerformance/Visualizations/JqplotGraph/StackedBarEvolution.php99
-rw-r--r--plugins/PagePerformance/Visualizations/PerformanceColumns.php76
-rw-r--r--plugins/PagePerformance/javascripts/PagePerformance.js141
-rw-r--r--plugins/PagePerformance/javascripts/jqplotStackedBarEvolutionGraph.js218
-rw-r--r--plugins/PagePerformance/javascripts/rowaction.js103
-rw-r--r--plugins/PagePerformance/lang/en.json39
-rw-r--r--plugins/PagePerformance/templates/getPagePerformancePopover.twig10
-rw-r--r--plugins/PagePerformance/tests/Fixtures/VisitsWithPagePerformanceMetrics.php118
-rw-r--r--plugins/PagePerformance/tests/Integration/Tracker/PerformanceDataProcessorTest.php109
-rw-r--r--plugins/PagePerformance/tests/System/APITest.php67
-rw-r--r--plugins/PagePerformance/tests/System/expected/test___Actions.getPageTitles_day.xml92
-rw-r--r--plugins/PagePerformance/tests/System/expected/test___Actions.getPageTitles_month.xml92
-rw-r--r--plugins/PagePerformance/tests/System/expected/test___Actions.getPageUrls_day.xml209
-rw-r--r--plugins/PagePerformance/tests/System/expected/test___Actions.getPageUrls_month.xml209
-rw-r--r--plugins/PagePerformance/tests/System/expected/test___PagePerformance.get_day.xml10
-rw-r--r--plugins/PagePerformance/tests/System/expected/test___PagePerformance.get_month.xml10
-rw-r--r--plugins/PagePerformance/tests/UI/PagePerformance_spec.js113
-rw-r--r--plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_load.png3
-rw-r--r--plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_pagetitle_overlay.png3
-rw-r--r--plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_pageurl_overlay.png3
-rw-r--r--plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_performance_visualization.png3
-rw-r--r--plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_rowactions.png3
-rw-r--r--plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_rowactions_subtable.png3
-rw-r--r--plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_visualizations.png3
47 files changed, 3684 insertions, 0 deletions
diff --git a/plugins/PagePerformance/API.php b/plugins/PagePerformance/API.php
new file mode 100644
index 0000000000..08f7c65ff0
--- /dev/null
+++ b/plugins/PagePerformance/API.php
@@ -0,0 +1,127 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance;
+
+use Piwik\Archive;
+use Piwik\Piwik;
+use Piwik\Plugin\ProcessedMetric;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AveragePageLoadTime;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeDomCompletion;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeDomProcessing;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeNetwork;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeServer;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeOnLoad;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeTransfer;
+
+/**
+ * @method static \Piwik\Plugins\PagePerformance\API getInstance()
+ */
+class API extends \Piwik\Plugin\API
+{
+ public function get($idSite, $period, $date, $segment = false)
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+
+ $archive = Archive::build($idSite, $period, $date, $segment);
+
+ $columns = array(
+ Archiver::PAGEPERFORMANCE_TOTAL_NETWORK_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_NETWORK_HITS,
+ Archiver::PAGEPERFORMANCE_TOTAL_SERVER_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_SERVER_HITS,
+ Archiver::PAGEPERFORMANCE_TOTAL_TRANSFER_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_TRANSFER_HITS,
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_HITS,
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_HITS,
+ Archiver::PAGEPERFORMANCE_TOTAL_ONLOAD_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_ONLOAD_HITS,
+ Archiver::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_HITS,
+ );
+
+ $dataTable = $archive->getDataTableFromNumeric($columns);
+
+ $precision = 2;
+
+ $dataTable->filter('ColumnCallbackReplace', [[
+ Archiver::PAGEPERFORMANCE_TOTAL_NETWORK_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_SERVER_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_TRANSFER_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_ONLOAD_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_TIME,
+ ], function($value) { return $value / 1000; }]);
+
+ $dataTable->filter('ColumnCallbackAddColumnQuotient', array(
+ $this->getMetricColumn(AverageTimeNetwork::class),
+ Archiver::PAGEPERFORMANCE_TOTAL_NETWORK_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_NETWORK_HITS,
+ $precision
+ ));
+
+ $dataTable->filter('ColumnCallbackAddColumnQuotient', array(
+ $this->getMetricColumn(AverageTimeServer::class),
+ Archiver::PAGEPERFORMANCE_TOTAL_SERVER_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_SERVER_HITS,
+ $precision
+ ));
+
+ $dataTable->filter('ColumnCallbackAddColumnQuotient', array(
+ $this->getMetricColumn(AverageTimeTransfer::class),
+ Archiver::PAGEPERFORMANCE_TOTAL_TRANSFER_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_TRANSFER_HITS,
+ $precision
+ ));
+
+ $dataTable->filter('ColumnCallbackAddColumnQuotient', array(
+ $this->getMetricColumn(AverageTimeDomProcessing::class),
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_HITS,
+ $precision
+ ));
+
+ $dataTable->filter('ColumnCallbackAddColumnQuotient', array(
+ $this->getMetricColumn(AverageTimeDomCompletion::class),
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_HITS,
+ $precision
+ ));
+
+ $dataTable->filter('ColumnCallbackAddColumnQuotient', array(
+ $this->getMetricColumn(AverageTimeOnLoad::class),
+ Archiver::PAGEPERFORMANCE_TOTAL_ONLOAD_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_ONLOAD_HITS,
+ $precision
+ ));
+
+ $dataTable->filter('ColumnCallbackAddColumnQuotient', array(
+ $this->getMetricColumn(AveragePageLoadTime::class),
+ Archiver::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_TIME,
+ Archiver::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_HITS,
+ $precision
+ ));
+
+ $dataTable->queueFilter('ColumnDelete', array($columns));
+
+ return $dataTable;
+ }
+
+ /**
+ * @param string $class
+ * @return string
+ */
+ private function getMetricColumn($class) {
+ /** @var ProcessedMetric $metric */
+ $metric = new $class();
+ return $metric->getName();
+ }
+}
diff --git a/plugins/PagePerformance/Archiver.php b/plugins/PagePerformance/Archiver.php
new file mode 100644
index 0000000000..36545fe566
--- /dev/null
+++ b/plugins/PagePerformance/Archiver.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\PagePerformance;
+
+use Piwik\Plugins\PagePerformance\Columns\TimeDomCompletion;
+use Piwik\Plugins\PagePerformance\Columns\TimeDomProcessing;
+use Piwik\Plugins\PagePerformance\Columns\TimeNetwork;
+use Piwik\Plugins\PagePerformance\Columns\TimeServer;
+use Piwik\Plugins\PagePerformance\Columns\TimeOnLoad;
+use Piwik\Plugins\PagePerformance\Columns\TimeTransfer;
+
+/**
+ * Class Archiver
+ */
+class Archiver extends \Piwik\Plugin\Archiver
+{
+ const PAGEPERFORMANCE_TOTAL_NETWORK_TIME = 'PagePerformance_network_time';
+ const PAGEPERFORMANCE_TOTAL_NETWORK_HITS = 'PagePerformance_network_hits';
+ const PAGEPERFORMANCE_TOTAL_SERVER_TIME = 'PagePerformance_servery_time';
+ const PAGEPERFORMANCE_TOTAL_SERVER_HITS = 'PagePerformance_server_hits';
+ const PAGEPERFORMANCE_TOTAL_TRANSFER_TIME = 'PagePerformance_transfer_time';
+ const PAGEPERFORMANCE_TOTAL_TRANSFER_HITS = 'PagePerformance_transfer_hits';
+ const PAGEPERFORMANCE_TOTAL_DOMPROCESSING_TIME = 'PagePerformance_domprocessing_time';
+ const PAGEPERFORMANCE_TOTAL_DOMPROCESSING_HITS = 'PagePerformance_domprocessing_hits';
+ const PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_TIME = 'PagePerformance_domcompletion_time';
+ const PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_HITS = 'PagePerformance_domcompletion_hits';
+ const PAGEPERFORMANCE_TOTAL_ONLOAD_TIME = 'PagePerformance_onload_time';
+ const PAGEPERFORMANCE_TOTAL_ONLOAD_HITS = 'PagePerformance_onload_hits';
+ const PAGEPERFORMANCE_TOTAL_PAGE_LOAD_TIME = 'PagePerformance_pageload_time';
+ const PAGEPERFORMANCE_TOTAL_PAGE_LOAD_HITS = 'PagePerformance_pageload_hits';
+
+ public function aggregateDayReport()
+ {
+ $selects = $totalColumns = $hitsColumns = [];
+ $table = 'log_link_visit_action';
+
+ $performanceDimensions = [
+ new TimeNetwork(),
+ new TimeServer(),
+ new TimeTransfer(),
+ new TimeDomProcessing(),
+ new TimeDomCompletion(),
+ new TimeOnLoad()
+ ];
+
+ foreach($performanceDimensions as $dimension) {
+ $column = $dimension->getColumnName();
+ $selects[] = "sum($table.$column) as {$column}_total";
+ $selects[] = "sum(if($table.$column is null, 0, 1)) as {$column}_hits";
+ $totalColumns[] = "$table.$column";
+ $hitsColumns[] = "if($table.$column is null, 0, 1)";
+ }
+
+ $selects[] = sprintf('SUM(%s) as page_load_total', implode(' + ', $totalColumns));
+ $selects[] = sprintf('(SUM(%s)/%s) as page_load_hits', implode(' + ', $hitsColumns), count($hitsColumns));
+
+ $joinLogActionOnColumn = array('idaction_url');
+
+ $query = $this->getLogAggregator()->queryActionsByDimension([], '', $selects, false, null, $joinLogActionOnColumn);
+
+ $result = $query->fetchAll();
+
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_NETWORK_TIME, 'time_network_total');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_NETWORK_HITS, 'time_network_hits');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_SERVER_TIME, 'time_server_total');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_SERVER_HITS, 'time_server_hits');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_TRANSFER_TIME, 'time_transfer_total');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_TRANSFER_HITS, 'time_transfer_hits');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_TIME, 'time_dom_processing_total');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_HITS, 'time_dom_processing_hits');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_TIME, 'time_dom_completion_total');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_HITS, 'time_dom_completion_hits');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_ONLOAD_TIME, 'time_on_load_total');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_ONLOAD_HITS, 'time_on_load_hits');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_TIME, 'page_load_total');
+ $this->sumAndInsertNumericRecord($result, self::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_HITS, 'page_load_hits');
+ }
+
+ /**
+ * @param $result
+ * @param string $metric
+ * @param string $field
+ */
+ private function sumAndInsertNumericRecord($result, $metric, $field)
+ {
+ $total = 0;
+
+ foreach ($result as $row) {
+ if (!empty($row[$field])) {
+ $total += (int) $row[$field];
+ }
+ }
+
+ $this->getProcessor()->insertNumericRecord($metric, $total);
+ }
+
+ public function aggregateMultipleReports()
+ {
+ $this->getProcessor()->aggregateNumericMetrics(array(
+ self::PAGEPERFORMANCE_TOTAL_NETWORK_TIME,
+ self::PAGEPERFORMANCE_TOTAL_NETWORK_HITS,
+ self::PAGEPERFORMANCE_TOTAL_SERVER_TIME,
+ self::PAGEPERFORMANCE_TOTAL_SERVER_HITS,
+ self::PAGEPERFORMANCE_TOTAL_TRANSFER_TIME,
+ self::PAGEPERFORMANCE_TOTAL_TRANSFER_HITS,
+ self::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_TIME,
+ self::PAGEPERFORMANCE_TOTAL_DOMPROCESSING_HITS,
+ self::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_TIME,
+ self::PAGEPERFORMANCE_TOTAL_DOMCOMPLETION_HITS,
+ self::PAGEPERFORMANCE_TOTAL_ONLOAD_TIME,
+ self::PAGEPERFORMANCE_TOTAL_ONLOAD_HITS,
+ self::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_TIME,
+ self::PAGEPERFORMANCE_TOTAL_PAGE_LOAD_HITS,
+ ));
+ }
+
+}
diff --git a/plugins/PagePerformance/Columns/Metrics/AveragePageLoadTime.php b/plugins/PagePerformance/Columns/Metrics/AveragePageLoadTime.php
new file mode 100644
index 0000000000..74fe50fdc2
--- /dev/null
+++ b/plugins/PagePerformance/Columns/Metrics/AveragePageLoadTime.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Columns\Metrics;
+
+use Piwik\DataTable\Row;
+use Piwik\Metrics\Formatter;
+use Piwik\Piwik;
+use Piwik\Plugin\ProcessedMetric;
+
+/**
+ * The average amount of time it took loading a page completely. Calculated as:
+ *
+ * avg_time_network + avg_time_server + avg_time_transfer + avg_time_dom_processing + avg_time_dom_completion + avg_time_on_load
+ */
+class AveragePageLoadTime extends ProcessedMetric
+{
+ public function getName()
+ {
+ return 'avg_page_load_time';
+ }
+
+ public function getTranslatedName()
+ {
+ return Piwik::translate('PagePerformance_ColumnAveragePageLoadTime');
+ }
+
+ public function getDocumentation()
+ {
+ return Piwik::translate('PagePerformance_ColumnAveragePageLoadTimeDocumentation');
+ }
+
+ public function compute(Row $row)
+ {
+ $sum = 0;
+ foreach ($this->getDependentMetrics() as $dependentMetric) {
+ $sum += self::getMetric($row, $dependentMetric);
+ }
+
+ return $sum;
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ if ($formatter instanceof Formatter\Html
+ && !$value
+ ) {
+ return '-';
+ } else {
+ return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = true);
+ }
+ }
+
+ public function getDependentMetrics()
+ {
+ return ['avg_time_network', 'avg_time_server', 'avg_time_transfer',
+ 'avg_time_dom_processing', 'avg_time_dom_completion', 'avg_time_on_load'];
+ }
+}
diff --git a/plugins/PagePerformance/Columns/Metrics/AveragePerformanceMetric.php b/plugins/PagePerformance/Columns/Metrics/AveragePerformanceMetric.php
new file mode 100644
index 0000000000..d7511fe840
--- /dev/null
+++ b/plugins/PagePerformance/Columns/Metrics/AveragePerformanceMetric.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Columns\Metrics;
+
+use Piwik\DataTable;
+use Piwik\DataTable\Row;
+use Piwik\Metrics\Formatter;
+use Piwik\Piwik;
+use Piwik\Plugin\ProcessedMetric;
+
+/**
+ * The average amount for a certain performance metric. Calculated as
+ *
+ * sum_time / nb_hits_with_time
+ *
+ * The above metrics are calculated during archiving. This metric is calculated before
+ * serving a report.
+ */
+abstract class AveragePerformanceMetric extends ProcessedMetric
+{
+ const ID = '';
+
+ public function getName()
+ {
+ return 'avg_' . static::ID;
+ }
+
+ public function getDependentMetrics()
+ {
+ return array('sum_' . static::ID, 'nb_hits_with_' . static::ID);
+ }
+
+ public function getTemporaryMetrics()
+ {
+ return array('sum_' . static::ID);
+ }
+
+ public function compute(Row $row)
+ {
+ $sumGenerationTime = $this->getMetric($row, 'sum_' . static::ID);
+ $hitsWithTimeGeneration = $this->getMetric($row, 'nb_hits_with_' . static::ID);
+
+ return Piwik::getQuotientSafe($sumGenerationTime, $hitsWithTimeGeneration, $precision = 3);
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ if ($formatter instanceof Formatter\Html
+ && !$value
+ ) {
+ return '-';
+ } else {
+ return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = true);
+ }
+ }
+
+ public function beforeCompute($report, DataTable $table)
+ {
+ $hasTimeGeneration = array_sum($this->getMetricValues($table, 'sum_' . static::ID)) > 0;
+
+ if (!$hasTimeGeneration
+ && $table->getRowsCount() != 0
+ && !$this->hasAverageMetric($table)
+ ) {
+ // No generation time: remove it from the API output and add it to empty_columns metadata, so that
+ // the columns can also be removed from the view
+ $table->filter('ColumnDelete', array(array(
+ 'sum_' . static::ID,
+ 'nb_hits_with_' . static::ID,
+ 'min_' . static::ID,
+ 'max_' . static::ID
+ )));
+
+ if ($table instanceof DataTable) {
+ $emptyColumns = $table->getMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME);
+ if (!is_array($emptyColumns)) {
+ $emptyColumns = array();
+ }
+ $emptyColumns[] = 'sum_' . static::ID;
+ $emptyColumns[] = 'nb_hits_with_' . static::ID;
+ $emptyColumns[] = 'min_' . static::ID;
+ $emptyColumns[] = 'max_' . static::ID;
+ $table->setMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME, $emptyColumns);
+ }
+ }
+
+ return $hasTimeGeneration;
+ }
+
+ private function hasAverageMetric(DataTable $table)
+ {
+ return $table->getFirstRow()->getColumn($this->getName()) !== false;
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/Columns/Metrics/AverageTimeDomCompletion.php b/plugins/PagePerformance/Columns/Metrics/AverageTimeDomCompletion.php
new file mode 100644
index 0000000000..e69ba699f4
--- /dev/null
+++ b/plugins/PagePerformance/Columns/Metrics/AverageTimeDomCompletion.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Columns\Metrics;
+
+use Piwik\Piwik;
+
+/**
+ * The average amount of time the browser needs to load media any Javascript listening for the DOMContentLoaded event.
+ * Calculated as
+ *
+ * sum_time_dom_completion / nb_hits_with_time_dom_completion
+ *
+ * The above metrics are calculated during archiving. This metric is calculated before
+ * serving a report.
+ */
+class AverageTimeDomCompletion extends AveragePerformanceMetric
+{
+ const ID = 'time_dom_completion';
+
+ public function getTranslatedName()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeDomCompletion');
+ }
+
+ public function getDocumentation()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeDomCompletionDocumentation');
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/Columns/Metrics/AverageTimeDomProcessing.php b/plugins/PagePerformance/Columns/Metrics/AverageTimeDomProcessing.php
new file mode 100644
index 0000000000..298867321e
--- /dev/null
+++ b/plugins/PagePerformance/Columns/Metrics/AverageTimeDomProcessing.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Columns\Metrics;
+
+use Piwik\Piwik;
+
+/**
+ * The average amount of time the browser spends until user can start interacting with the page. Calculated as
+ *
+ * sum_time_dom_processing / nb_hits_with_time_dom_processing
+ *
+ * The above metrics are calculated during archiving. This metric is calculated before
+ * serving a report.
+ */
+class AverageTimeDomProcessing extends AveragePerformanceMetric
+{
+ const ID = 'time_dom_processing';
+
+ public function getTranslatedName()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeDomProcessing');
+ }
+
+ public function getDocumentation()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeDomProcessingDocumentation');
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/Columns/Metrics/AverageTimeNetwork.php b/plugins/PagePerformance/Columns/Metrics/AverageTimeNetwork.php
new file mode 100644
index 0000000000..dbc1d051e5
--- /dev/null
+++ b/plugins/PagePerformance/Columns/Metrics/AverageTimeNetwork.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Columns\Metrics;
+
+use Piwik\Piwik;
+
+/**
+ * The average amount of time needed to connect to the server. Calculated as
+ *
+ * sum_time_network / nb_hits_with_time_network
+ *
+ * The above metrics are calculated during archiving. This metric is calculated before
+ * serving a report.
+ */
+class AverageTimeNetwork extends AveragePerformanceMetric
+{
+ const ID = 'time_network';
+
+ public function getTranslatedName()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeNetwork');
+ }
+
+ public function getDocumentation()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeNetworkDocumentation');
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/Columns/Metrics/AverageTimeOnLoad.php b/plugins/PagePerformance/Columns/Metrics/AverageTimeOnLoad.php
new file mode 100644
index 0000000000..e700b833bb
--- /dev/null
+++ b/plugins/PagePerformance/Columns/Metrics/AverageTimeOnLoad.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Columns\Metrics;
+
+use Piwik\Piwik;
+
+/**
+ * The average amount of time browser needs to execute javascript waiting for window.load event. Calculated as
+ *
+ * sum_time_on_load / nb_hits_with_time_on_load
+ *
+ * The above metrics are calculated during archiving. This metric is calculated before
+ * serving a report.
+ */
+class AverageTimeOnLoad extends AveragePerformanceMetric
+{
+ const ID = 'time_on_load';
+
+ public function getTranslatedName()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeOnLoad');
+ }
+
+ public function getDocumentation()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeOnLoadDocumentation');
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/Columns/Metrics/AverageTimeServer.php b/plugins/PagePerformance/Columns/Metrics/AverageTimeServer.php
new file mode 100644
index 0000000000..26da8df8f3
--- /dev/null
+++ b/plugins/PagePerformance/Columns/Metrics/AverageTimeServer.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Columns\Metrics;
+
+use Piwik\Piwik;
+
+/**
+ * The average amount of time the server needs to start serving a page. Calculated as
+ *
+ * sum_time_server / nb_hits_with_time_server
+ *
+ * The above metrics are calculated during archiving. This metric is calculated before
+ * serving a report.
+ */
+class AverageTimeServer extends AveragePerformanceMetric
+{
+ const ID = 'time_server';
+
+ public function getTranslatedName()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeServer');
+ }
+
+ public function getDocumentation()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeServerDocumentation');
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/Columns/Metrics/AverageTimeTransfer.php b/plugins/PagePerformance/Columns/Metrics/AverageTimeTransfer.php
new file mode 100644
index 0000000000..01aa7398c0
--- /dev/null
+++ b/plugins/PagePerformance/Columns/Metrics/AverageTimeTransfer.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Columns\Metrics;
+
+use Piwik\Piwik;
+
+/**
+ * The average amount of time it takes to transfer a page. Calculated as
+ *
+ * sum_time_transfer / nb_hits_with_time_transfer
+ *
+ * The above metrics are calculated during archiving. This metric is calculated before
+ * serving a report.
+ */
+class AverageTimeTransfer extends AveragePerformanceMetric
+{
+ const ID = 'time_transfer';
+
+ public function getTranslatedName()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeTransfer');
+ }
+
+ public function getDocumentation()
+ {
+ return Piwik::translate('PagePerformance_ColumnAverageTimeTransferDocumentation');
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/Columns/TimeDomCompletion.php b/plugins/PagePerformance/Columns/TimeDomCompletion.php
new file mode 100644
index 0000000000..45c15a9c42
--- /dev/null
+++ b/plugins/PagePerformance/Columns/TimeDomCompletion.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\Columns;
+
+use Piwik\Columns\DimensionMetricFactory;
+use Piwik\Columns\MetricsList;
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\ComputedMetric;
+use Piwik\Plugin\Dimension\ActionDimension;
+use Piwik\Tracker\Action;
+use Piwik\Tracker\ActionPageview;
+use Piwik\Tracker\Request;
+use Piwik\Tracker\Visitor;
+
+class TimeDomCompletion extends ActionDimension
+{
+ protected $columnName = 'time_dom_completion';
+ protected $columnType = 'MEDIUMINT(10) UNSIGNED NULL';
+ protected $type = self::TYPE_DURATION_MS;
+ protected $nameSingular = 'PagePerformance_ColumnTimeDomCompletion';
+
+ public function onNewAction(Request $request, Visitor $visitor, Action $action)
+ {
+ if (!($action instanceof ActionPageview)) {
+ return false;
+ }
+
+ $domCompleteTime = $request->getParam($this->getRequestParam());
+
+ if ($domCompleteTime === -1) {
+ return false;
+ }
+
+ return $domCompleteTime;
+ }
+
+ public function getRequestParam()
+ {
+ return 'pf_dm2';
+ }
+
+ public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
+ {
+ $metric1 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
+ $metric1->setName('sum_time_dom_completion');
+ $metricsList->addMetric($metric1);
+
+ $metric2 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX);
+ $metric2->setName('max_time_dom_completion');
+ $metricsList->addMetric($metric2);
+
+ $metric3 = $dimensionMetricFactory->createMetric('sum(if(%s is null, 0, 1))');
+ $metric3->setName('pageviews_with_time_dom_completion');
+ $metric3->setTranslatedName(Piwik::translate('PagePerformance_ColumnViewsWithDomCompletionTime'));
+ $metricsList->addMetric($metric3);
+
+ $metric4 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MIN);
+ $metric4->setName('min_time_dom_completion');
+ $metricsList->addMetric($metric4);
+
+ $metric = $dimensionMetricFactory->createComputedMetric($metric1->getName(), $metric3->getName(), ComputedMetric::AGGREGATION_AVG);
+ $metric->setName('avg_page_time_dom_completion');
+ $metric->setTranslatedName(Piwik::translate('PagePerformance_ColumnAverageDomCompletionTime'));
+ $metric->setDocumentation(Piwik::translate('PagePerformance_ColumnAverageDomCompletionTimeDocumentation'));
+ $metricsList->addMetric($metric);
+ }
+}
diff --git a/plugins/PagePerformance/Columns/TimeDomProcessing.php b/plugins/PagePerformance/Columns/TimeDomProcessing.php
new file mode 100644
index 0000000000..4fa8db2048
--- /dev/null
+++ b/plugins/PagePerformance/Columns/TimeDomProcessing.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\Columns;
+
+use Piwik\Columns\DimensionMetricFactory;
+use Piwik\Columns\MetricsList;
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\ComputedMetric;
+use Piwik\Plugin\Dimension\ActionDimension;
+use Piwik\Tracker\Action;
+use Piwik\Tracker\ActionPageview;
+use Piwik\Tracker\Request;
+use Piwik\Tracker\Visitor;
+
+class TimeDomProcessing extends ActionDimension
+{
+ protected $columnName = 'time_dom_processing';
+ protected $columnType = 'MEDIUMINT(10) UNSIGNED NULL';
+ protected $type = self::TYPE_DURATION_MS;
+ protected $nameSingular = 'PagePerformance_ColumnTimeDomProcessing';
+
+ public function onNewAction(Request $request, Visitor $visitor, Action $action)
+ {
+ if (!($action instanceof ActionPageview)) {
+ return false;
+ }
+
+ $domProcessTime = $request->getParam($this->getRequestParam());
+
+ if ($domProcessTime === -1) {
+ return false;
+ }
+
+ return $domProcessTime;
+ }
+
+ public function getRequestParam()
+ {
+ return 'pf_dm1';
+ }
+
+ public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
+ {
+ $metric1 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
+ $metric1->setName('sum_time_dom_processing');
+ $metricsList->addMetric($metric1);
+
+ $metric2 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX);
+ $metric2->setName('max_time_dom_processing');
+ $metricsList->addMetric($metric2);
+
+ $metric3 = $dimensionMetricFactory->createMetric('sum(if(%s is null, 0, 1))');
+ $metric3->setName('pageviews_with_time_dom_processing');
+ $metric3->setTranslatedName(Piwik::translate('PagePerformance_ColumnViewsWithDomProcessingTime'));
+ $metricsList->addMetric($metric3);
+
+ $metric4 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MIN);
+ $metric4->setName('min_time_dom_processing');
+ $metricsList->addMetric($metric4);
+
+ $metric = $dimensionMetricFactory->createComputedMetric($metric1->getName(), $metric3->getName(), ComputedMetric::AGGREGATION_AVG);
+ $metric->setName('avg_time_dom_processing');
+ $metric->setTranslatedName(Piwik::translate('PagePerformance_ColumnAverageDomProcessingTime'));
+ $metric->setDocumentation(Piwik::translate('PagePerformance_ColumnAverageDomProcessingTimeDocumentation'));
+ $metricsList->addMetric($metric);
+ }
+}
diff --git a/plugins/PagePerformance/Columns/TimeNetwork.php b/plugins/PagePerformance/Columns/TimeNetwork.php
new file mode 100644
index 0000000000..d1166a36c6
--- /dev/null
+++ b/plugins/PagePerformance/Columns/TimeNetwork.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\Columns;
+
+use Piwik\Columns\DimensionMetricFactory;
+use Piwik\Columns\MetricsList;
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\ComputedMetric;
+use Piwik\Plugin\Dimension\ActionDimension;
+use Piwik\Tracker\Action;
+use Piwik\Tracker\ActionPageview;
+use Piwik\Tracker\Request;
+use Piwik\Tracker\Visitor;
+
+class TimeNetwork extends ActionDimension
+{
+ protected $columnName = 'time_network';
+ protected $columnType = 'MEDIUMINT(10) UNSIGNED NULL';
+ protected $type = self::TYPE_DURATION_MS;
+ protected $nameSingular = 'PagePerformance_ColumnTimeNetwork';
+
+ public function onNewAction(Request $request, Visitor $visitor, Action $action)
+ {
+ if (!($action instanceof ActionPageview)) {
+ return false;
+ }
+
+ $networkTime = $request->getParam($this->getRequestParam());
+
+ if ($networkTime === -1) {
+ return false;
+ }
+
+ return $networkTime;
+ }
+
+ public function getRequestParam()
+ {
+ return 'pf_net';
+ }
+
+ public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
+ {
+ $metric1 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
+ $metric1->setName('sum_time_network');
+ $metricsList->addMetric($metric1);
+
+ $metric2 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX);
+ $metric2->setName('max_time_network');
+ $metricsList->addMetric($metric2);
+
+ $metric3 = $dimensionMetricFactory->createMetric('sum(if(%s is null, 0, 1))');
+ $metric3->setName('pageviews_with_time_network');
+ $metric3->setTranslatedName(Piwik::translate('PagePerformance_ColumnViewsWithNetworkTime'));
+ $metricsList->addMetric($metric3);
+
+ $metric4 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MIN);
+ $metric4->setName('min_time_network');
+ $metricsList->addMetric($metric4);
+
+ $metric = $dimensionMetricFactory->createComputedMetric($metric1->getName(), $metric3->getName(), ComputedMetric::AGGREGATION_AVG);
+ $metric->setName('avg_time_network');
+ $metric->setTranslatedName(Piwik::translate('PagePerformance_ColumnAverageNetworkTime'));
+ $metric->setDocumentation(Piwik::translate('PagePerformance_ColumnAverageNetworkTimeDocumentation'));
+ $metricsList->addMetric($metric);
+ }
+}
diff --git a/plugins/PagePerformance/Columns/TimeOnLoad.php b/plugins/PagePerformance/Columns/TimeOnLoad.php
new file mode 100644
index 0000000000..bb64b3d2fe
--- /dev/null
+++ b/plugins/PagePerformance/Columns/TimeOnLoad.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\Columns;
+
+use Piwik\Columns\DimensionMetricFactory;
+use Piwik\Columns\MetricsList;
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\ComputedMetric;
+use Piwik\Plugin\Dimension\ActionDimension;
+use Piwik\Tracker\Action;
+use Piwik\Tracker\ActionPageview;
+use Piwik\Tracker\Request;
+use Piwik\Tracker\Visitor;
+
+class TimeOnLoad extends ActionDimension
+{
+ protected $columnName = 'time_on_load';
+ protected $columnType = 'MEDIUMINT(10) UNSIGNED NULL';
+ protected $type = self::TYPE_DURATION_MS;
+ protected $nameSingular = 'PagePerformance_ColumnTimeOnLoad';
+
+ public function onNewAction(Request $request, Visitor $visitor, Action $action)
+ {
+ if (!($action instanceof ActionPageview)) {
+ return false;
+ }
+
+ $timeOnLoad = $request->getParam($this->getRequestParam());
+
+ if ($timeOnLoad === -1) {
+ return null;
+ }
+
+ return $timeOnLoad;
+ }
+
+ public function getRequestParam()
+ {
+ return 'pf_onl';
+ }
+
+ public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
+ {
+ $metric1 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
+ $metric1->setName('sum_time_on_load');
+ $metricsList->addMetric($metric1);
+
+ $metric2 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX);
+ $metric2->setName('max_time_on_load');
+ $metricsList->addMetric($metric2);
+
+ $metric3 = $dimensionMetricFactory->createMetric('sum(if(%s is null, 0, 1))');
+ $metric3->setName('pageviews_with_time_on_load');
+ $metric3->setTranslatedName(Piwik::translate('PagePerformance_ColumnViewsWithOnLoadTime'));
+ $metricsList->addMetric($metric3);
+
+ $metric4 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MIN);
+ $metric4->setName('min_time_on_load');
+ $metricsList->addMetric($metric4);
+
+ $metric = $dimensionMetricFactory->createComputedMetric($metric1->getName(), $metric3->getName(), ComputedMetric::AGGREGATION_AVG);
+ $metric->setName('avg_time_on_load');
+ $metric->setTranslatedName(Piwik::translate('PagePerformance_ColumnAverageOnLoadTime'));
+ $metric->setDocumentation(Piwik::translate('PagePerformance_ColumnAverageOnLoadTimeDocumentation'));
+ $metricsList->addMetric($metric);
+ }
+}
diff --git a/plugins/PagePerformance/Columns/TimeServer.php b/plugins/PagePerformance/Columns/TimeServer.php
new file mode 100644
index 0000000000..baf3097c9d
--- /dev/null
+++ b/plugins/PagePerformance/Columns/TimeServer.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\Columns;
+
+use Piwik\Columns\DimensionMetricFactory;
+use Piwik\Columns\MetricsList;
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\ComputedMetric;
+use Piwik\Plugin\Dimension\ActionDimension;
+use Piwik\Tracker\Action;
+use Piwik\Tracker\ActionPageview;
+use Piwik\Tracker\Request;
+use Piwik\Tracker\Visitor;
+
+class TimeServer extends ActionDimension
+{
+ protected $columnName = 'time_server';
+ protected $columnType = 'MEDIUMINT(10) UNSIGNED NULL';
+ protected $type = self::TYPE_DURATION_MS;
+ protected $nameSingular = 'PagePerformance_ColumnTimeServer';
+
+ public function onNewAction(Request $request, Visitor $visitor, Action $action)
+ {
+ if (!($action instanceof ActionPageview)) {
+ return false;
+ }
+
+ $serverTime = $request->getParam($this->getRequestParam());
+
+ if ($serverTime === -1) {
+ return false;
+ }
+
+ return $serverTime;
+ }
+
+ public function getRequestParam()
+ {
+ return 'pf_srv';
+ }
+
+ public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
+ {
+ $metric1 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
+ $metric1->setName('sum_time_server');
+ $metricsList->addMetric($metric1);
+
+ $metric2 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX);
+ $metric2->setName('max_time_server');
+ $metricsList->addMetric($metric2);
+
+ $metric3 = $dimensionMetricFactory->createMetric('sum(if(%s is null, 0, 1))');
+ $metric3->setName('pageviews_with_time_server');
+ $metric3->setTranslatedName(Piwik::translate('PagePerformance_ColumnViewsWithServerTime'));
+ $metricsList->addMetric($metric3);
+
+ $metric4 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MIN);
+ $metric4->setName('min_time_server');
+ $metricsList->addMetric($metric4);
+
+ $metric = $dimensionMetricFactory->createComputedMetric($metric1->getName(), $metric3->getName(), ComputedMetric::AGGREGATION_AVG);
+ $metric->setName('avg_time_server');
+ $metric->setTranslatedName(Piwik::translate('PagePerformance_ColumnAverageServerTime'));
+ $metric->setDocumentation(Piwik::translate('PagePerformance_ColumnAverageServerTimeDocumentation'));
+ $metricsList->addMetric($metric);
+ }
+}
diff --git a/plugins/PagePerformance/Columns/TimeTransfer.php b/plugins/PagePerformance/Columns/TimeTransfer.php
new file mode 100644
index 0000000000..43a7fe0535
--- /dev/null
+++ b/plugins/PagePerformance/Columns/TimeTransfer.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\Columns;
+
+use Piwik\Columns\DimensionMetricFactory;
+use Piwik\Columns\MetricsList;
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\ComputedMetric;
+use Piwik\Plugin\Dimension\ActionDimension;
+use Piwik\Tracker\Action;
+use Piwik\Tracker\ActionPageview;
+use Piwik\Tracker\Request;
+use Piwik\Tracker\Visitor;
+
+class TimeTransfer extends ActionDimension
+{
+ protected $columnName = 'time_transfer';
+ protected $columnType = 'MEDIUMINT(10) UNSIGNED NULL';
+ protected $type = self::TYPE_DURATION_MS;
+ protected $nameSingular = 'PagePerformance_ColumnTimeTransfer';
+
+ public function onNewAction(Request $request, Visitor $visitor, Action $action)
+ {
+ if (!($action instanceof ActionPageview)) {
+ return false;
+ }
+
+ $transferTime = $request->getParam($this->getRequestParam());
+
+ if ($transferTime === -1) {
+ return false;
+ }
+
+ return $transferTime;
+ }
+
+ public function getRequestParam()
+ {
+ return 'pf_tfr';
+ }
+
+ public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
+ {
+ $metric1 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
+ $metric1->setName('sum_time_transfer');
+ $metricsList->addMetric($metric1);
+
+ $metric2 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX);
+ $metric2->setName('max_time_transfer');
+ $metricsList->addMetric($metric2);
+
+ $metric3 = $dimensionMetricFactory->createMetric('sum(if(%s is null, 0, 1))');
+ $metric3->setName('pageviews_with_time_transfer');
+ $metric3->setTranslatedName(Piwik::translate('PagePerformance_ColumnViewsWithTransferTime'));
+ $metricsList->addMetric($metric3);
+
+ $metric4 = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MIN);
+ $metric4->setName('min_time_transfer');
+ $metricsList->addMetric($metric4);
+
+ $metric = $dimensionMetricFactory->createComputedMetric($metric1->getName(), $metric3->getName(), ComputedMetric::AGGREGATION_AVG);
+ $metric->setName('avg_time_transfer');
+ $metric->setTranslatedName(Piwik::translate('PagePerformance_ColumnAverageTransferTime'));
+ $metric->setDocumentation(Piwik::translate('PagePerformance_ColumnAverageTransferTimeDocumentation'));
+ $metricsList->addMetric($metric);
+ }
+}
diff --git a/plugins/PagePerformance/Controller.php b/plugins/PagePerformance/Controller.php
new file mode 100644
index 0000000000..7e087b7eed
--- /dev/null
+++ b/plugins/PagePerformance/Controller.php
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance;
+
+use Piwik\API\Request;
+use Piwik\Common;
+use Piwik\DataTable;
+use Piwik\Metrics\Formatter;
+use Piwik\Piwik;
+use Piwik\Plugin\Controller as PluginController;
+use Piwik\Plugin\ReportsProvider;
+use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz;
+use Piwik\Plugins\PagePerformance\Visualizations\JqplotGraph\StackedBarEvolution;
+use Piwik\View;
+use Piwik\ViewDataTable\Factory as ViewDataTableFactory;
+
+class Controller extends PluginController
+{
+ public function indexPagePerformance()
+ {
+ $this->checkSitePermission();
+
+ $view = new View('@PagePerformance/getPagePerformancePopover');
+
+ $dataTable = $this->getEvolutionTable();
+
+ $view->graph = $this->getRowEvolutionGraph($dataTable);
+
+ $view->metrics = '';
+
+ $metrics = Metrics::getPagePerformanceMetrics();
+
+ foreach ($metrics as $metric) {
+ $view->metrics .= sprintf('<strong>%s</strong>: %s<br />', $metric->getTranslatedName(), $metric->getDocumentation());
+ }
+
+ return $view->render();
+ }
+
+ protected function getEvolutionTable()
+ {
+ $apiMethod = Common::getRequestVar('apiMethod');
+ $period = Common::getRequestVar('period');
+
+ $params = [
+ 'method' => $apiMethod,
+ 'period' => $period,
+ 'label' => Common::getRequestVar('label', ''),
+ 'idSite' => $this->idSite,
+ 'segment' => Common::getRequestVar('segment', ''),
+ 'date' => 'range' === $period ? $this->strDate : EvolutionViz::getDateRangeAndLastN($period, $this->strDate)[0],
+ 'format' => 'original',
+ 'serialize' => '0',
+ ];
+
+ /** @var DataTable $dataTable */
+ $dataTable = Request::processRequest($apiMethod, $params, []);
+ $dataTable->deleteColumn('label');
+
+ return $dataTable;
+ }
+
+ /**
+ * @return string|void
+ * @throws \Exception
+ */
+ public function getRowEvolutionGraph($dataTable=null)
+ {
+ $this->checkSitePermission();
+
+ $apiMethod = Common::getRequestVar('apiMethod');
+
+ if (empty($dataTable)) {
+ $dataTable = $this->getEvolutionTable();
+ }
+
+ // set up the view data table
+ $view = ViewDataTableFactory::build(
+ StackedBarEvolution::ID, $apiMethod, 'PagePerformance.getRowEvolutionGraph', $forceDefault = true);
+ $view->setDataTable($dataTable);
+
+ $view->config->columns_to_display = array_keys(Metrics::getPagePerformanceMetrics());
+
+ $view->requestConfig->request_parameters_to_modify['label'] = '';
+ $view->config->show_goals = false;
+ $view->config->show_search = false;
+ $view->config->show_all_views_icons = false;
+ $view->config->show_related_reports = false;
+ $view->config->show_series_picker = false;
+ $view->config->show_footer_message = false;
+ $view->config->selectable_columns = array_keys(Metrics::getPagePerformanceMetrics());
+
+ return $this->renderView($view);
+ }
+
+ public function getEvolutionGraph()
+ {
+ $this->checkSitePermission();
+
+ $columns = Common::getRequestVar('columns', false);
+ if (false !== $columns) {
+ $columns = Piwik::getArrayFromApiParameter($columns);
+ }
+ $view = ViewDataTableFactory::build(
+ StackedBarEvolution::ID,
+ 'PagePerformance.get',
+ $this->pluginName . '.' . __FUNCTION__,
+ $forceDefault = true
+ );
+ $view->config->show_goals = false;
+
+ $performanceMetrics = array_keys(Metrics::getPagePerformanceMetrics());
+
+ if (!empty($columns)) {
+ $view->config->columns_to_display = array_intersect($columns, $performanceMetrics);
+ }
+
+ if (empty($view->config->columns_to_display)) {
+ $view->config->columns_to_display = $performanceMetrics;
+ }
+
+ $view->config->documentation = Piwik::translate('PagePerformance_EvolutionOverPeriod') . '<br /><br />';
+
+ $metrics = Metrics::getPagePerformanceMetrics();
+
+ foreach ($metrics as $metric) {
+ $view->config->documentation .= sprintf('<strong>%s</strong>: %s<br />', $metric->getTranslatedName(), $metric->getDocumentation());
+ }
+
+ $report = ReportsProvider::factory('PagePerformance', 'get');
+ $view->config->selectable_columns = array_keys(Metrics::getPagePerformanceMetrics());
+
+ $numberFormatter = new Formatter\Html();
+ $metrics = $report->getMetrics();
+ $view->config->filters[] = function (DataTable $table) use ($numberFormatter, $metrics) {
+ $firstRow = $table->getFirstRow();
+ if ($firstRow) {
+ foreach ($metrics as $metric => $name) {
+ $metricValue = $firstRow->getColumn($metric);
+ if (false !== $metricValue) {
+ $firstRow->setColumn($metric, $numberFormatter->getPrettyTimeFromSeconds($metricValue));
+ }
+ }
+ }
+ };
+
+ $view->config->addTranslations(Metrics::getMetricTranslations());
+
+ return $this->renderView($view);
+ }
+}
diff --git a/plugins/PagePerformance/JqplotDataGenerator/Chart.php b/plugins/PagePerformance/JqplotDataGenerator/Chart.php
new file mode 100644
index 0000000000..b1e2d43df7
--- /dev/null
+++ b/plugins/PagePerformance/JqplotDataGenerator/Chart.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\JqplotDataGenerator;
+
+use Piwik\Common;
+use Piwik\ProxyHttp;
+
+/**
+ *
+ */
+class Chart extends \Piwik\Plugins\CoreVisualizations\JqplotDataGenerator\Chart
+{
+ // the data kept here conforms to the jqplot data layout
+ // @see http://www.jqplot.com/docs/files/jqPlotOptions-txt.html
+ protected $series = array();
+ protected $data = array();
+ protected $axes = array();
+
+ // temporary
+ public $properties;
+
+ public function setAxisXLabels($xLabels, $xTicks = null, $index = 0)
+ {
+ $axisName = $this->getXAxis($index);
+
+ $xSteps = $this->properties['x_axis_step_size'];
+ $showAllTicks = $this->properties['show_all_ticks'];
+
+ $this->axes[$axisName]['labels'] = array_values($xLabels);
+
+ $ticks = array_values($xTicks ?: $xLabels);
+
+ if (!$showAllTicks) {
+ // unset labels so there are $xSteps number of blank ticks between labels
+ foreach ($ticks as $i => &$label) {
+ if ($i % $xSteps != 0) {
+ $label = ' ';
+ }
+ }
+ }
+ $this->axes[$axisName]['ticks'] = $ticks;
+ }
+
+ public function setAxisXOnClick(&$onClick)
+ {
+ $this->axes['xaxis']['onclick'] = & $onClick;
+ }
+
+ public function setAxisYValues(&$values, $seriesLabels = null)
+ {
+ $this->series = $seriesLabels;
+ array_walk_recursive($values, function (&$v) {
+ $v = (float) Common::forceDotAsSeparatorForDecimalPoint($v);
+ });
+ $this->data = &$values;
+ }
+
+ public function setAxisYUnits($yUnits)
+ {
+ $yUnits = array_values(array_map('strval', $yUnits));
+
+ // generate axis IDs for each unique y unit
+ $axesIds = array();
+ foreach ($yUnits as $idx => $unit) {
+ if (!isset($axesIds[$unit])) {
+ // handle axes ids: first y[]axis, then y[2]axis, y[3]axis...
+ $nextAxisId = empty($axesIds) ? '' : count($axesIds) + 1;
+
+ $axesIds[$unit] = 'y' . $nextAxisId . 'axis';
+ }
+ }
+
+ // generate jqplot axes config
+ foreach ($axesIds as $unit => $axisId) {
+ $this->axes[$axisId]['tickOptions']['formatString'] = '%s' . $unit;
+ }
+
+ // map each series to appropriate yaxis
+ foreach ($yUnits as $idx => $unit) {
+ $this->series[$idx]['yaxis'] = $axesIds[$unit];
+ }
+ }
+
+ public function setAxisYLabels($labels)
+ {
+ foreach ($this->series as &$series) {
+ $label = $series['internalLabel'];
+ if (isset($labels[$label])) {
+ $series['label'] = $labels[$label];
+ }
+ }
+ }
+
+ public function render()
+ {
+ ProxyHttp::overrideCacheControlHeaders();
+
+ // See http://www.jqplot.com/docs/files/jqPlotOptions-txt.html
+ $data = array(
+ 'params' => array(
+ 'axes' => &$this->axes,
+ 'series' => &$this->series
+ ),
+ 'data' => &$this->data
+ );
+
+ return $data;
+ }
+
+ public function setAxisXLabelsMultiple($xLabels, $seriesToXAxis, $ticks = null)
+ {
+ foreach ($xLabels as $index => $labels) {
+ $this->setAxisXLabels($labels, $ticks === null ? null : $ticks[$index], $index);
+ }
+
+ foreach ($seriesToXAxis as $seriesIndex => $xAxisIndex) {
+ $axisName = $this->getXAxis($xAxisIndex);
+
+ // don't actually set xaxis otherwise jqplot will show too many axes. however, we need the xaxis labels, so we add them
+ // to the jqplot config
+ $this->series[$seriesIndex]['_xaxis'] = $axisName;
+ }
+ }
+
+ private function getXAxis($index)
+ {
+ $axisName = 'xaxis';
+ if ($index != 0) {
+ $axisName = 'x' . ($index + 1) . 'axis';
+ }
+ return $axisName;
+ }
+}
diff --git a/plugins/PagePerformance/JqplotDataGenerator/StackedBarEvolution.php b/plugins/PagePerformance/JqplotDataGenerator/StackedBarEvolution.php
new file mode 100644
index 0000000000..203753af36
--- /dev/null
+++ b/plugins/PagePerformance/JqplotDataGenerator/StackedBarEvolution.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\JqplotDataGenerator;
+
+use Piwik\Archive\DataTableFactory;
+use Piwik\Common;
+use Piwik\DataTable;
+use Piwik\Date;
+use Piwik\Metrics;
+use Piwik\Period;
+use Piwik\Period\Factory;
+use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator;
+use Piwik\Url;
+
+/**
+ * Generates JQPlot JSON data/config for evolution graphs.
+ */
+class StackedBarEvolution extends JqplotDataGenerator\Evolution
+{
+ public function generate($dataTable)
+ {
+ $visualization = new Chart();
+
+ if ($dataTable->getRowsCount() > 0) {
+ $dataTable->applyQueuedFilters();
+ $this->initChartObjectData($dataTable, $visualization);
+ }
+
+ return $visualization->render();
+ }
+
+ /**
+ * @param DataTable|DataTable\Map $dataTable
+ * @param Chart $visualization
+ */
+ protected function initChartObjectData($dataTable, $visualization)
+ {
+ // if the loaded datatable is a simple DataTable, it is most likely a plugin plotting some custom data
+ // we don't expect plugin developers to return a well defined Set
+
+ if ($dataTable instanceof DataTable) {
+ parent::initChartObjectData($dataTable, $visualization);
+ return;
+ }
+
+ $dataTables = $dataTable->getDataTables();
+
+ // determine x labels based on both the displayed date range and the compared periods
+ /** @var Period[][] $xLabels */
+ $xLabels = [
+ [], // placeholder for first series
+ ];
+
+ $this->addSelectedSeriesXLabels($xLabels, $dataTables);
+
+ $columnsToDisplay = array_values($this->properties['columns_to_display']);
+
+ // collect series data to show. each row-to-display/column-to-display permutation creates a series.
+ $allSeriesData = array();
+ foreach ($columnsToDisplay as $column) {
+ $allSeriesData[] = $this->getSeriesData($column, $dataTable);
+ }
+
+ $visualization->dataTable = $dataTable;
+ $visualization->properties = $this->properties;
+
+ $seriesLabels = [];
+ foreach ($columnsToDisplay as $columnName) {
+ $seriesLabels[] = [
+ 'internalLabel' => $columnName,
+ 'label' => @$this->properties['translations'][$columnName] ?: $columnName
+ ];
+ }
+
+ $visualization->setAxisYValues($allSeriesData, $seriesLabels);
+ $visualization->setAxisYUnits($this->getUnitsForColumnsToDisplay());
+
+ $xLabelStrs = [];
+ $xAxisTicks = [];
+ foreach ($xLabels as $index => $seriesXLabels) {
+ $xLabelStrs[$index] = array_map(function (Period $p) { return $p->getLocalizedLongString(); }, $seriesXLabels);
+ $xAxisTicks[$index] = array_map(function (Period $p) { return $p->getLocalizedShortString(); }, $seriesXLabels);
+ }
+
+ $visualization->setAxisXLabelsMultiple($xLabelStrs, [], $xAxisTicks);
+ }
+
+ private function getSeriesData($column, DataTable\Map $dataTable)
+ {
+ $seriesData = array();
+ foreach ($dataTable->getDataTables() as $childTable) {
+ $row = $childTable->getFirstRow();
+
+ // get series data point. defaults to 0 if no row or no column value.
+ if ($row === false) {
+ $seriesData[] = 0;
+ } else {
+ $seriesData[] = $row->getColumn($column) ? : 0;
+ }
+ }
+
+ return $seriesData;
+ }
+}
diff --git a/plugins/PagePerformance/Metrics.php b/plugins/PagePerformance/Metrics.php
new file mode 100644
index 0000000000..1744e847a4
--- /dev/null
+++ b/plugins/PagePerformance/Metrics.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance;
+
+use Piwik\Plugin\Dimension\ActionDimension;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AveragePageLoadTime;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeDomCompletion;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeDomProcessing;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeNetwork;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeServer;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeOnLoad;
+use Piwik\Plugins\PagePerformance\Columns\Metrics\AverageTimeTransfer;
+use Piwik\Plugins\PagePerformance\Columns\TimeDomCompletion;
+use Piwik\Plugins\PagePerformance\Columns\TimeDomProcessing;
+use Piwik\Plugins\PagePerformance\Columns\TimeNetwork;
+use Piwik\Plugins\PagePerformance\Columns\TimeServer;
+use Piwik\Plugins\PagePerformance\Columns\TimeOnLoad;
+use Piwik\Plugins\PagePerformance\Columns\TimeTransfer;
+
+class Metrics
+{
+ /**
+ * @return \Piwik\Plugins\PagePerformance\Columns\Metrics\AveragePerformanceMetric[]
+ */
+ public static function getPagePerformanceMetrics()
+ {
+ $metrics = [
+ new AverageTimeNetwork(),
+ new AverageTimeServer(),
+ new AverageTimeTransfer(),
+ new AverageTimeDomProcessing(),
+ new AverageTimeDomCompletion(),
+ new AverageTimeOnLoad(),
+ ];
+
+ $mappedMetrics = [];
+
+ foreach ($metrics as $metric) {
+ $mappedMetrics[$metric->getName()] = $metric;
+ }
+
+ return $mappedMetrics;
+ }
+
+ /**
+ * @return \Piwik\Plugins\PagePerformance\Columns\Metrics\AveragePerformanceMetric[]
+ */
+ public static function getAllPagePerformanceMetrics()
+ {
+ $metrics = [
+ new AverageTimeNetwork(),
+ new AverageTimeServer(),
+ new AverageTimeTransfer(),
+ new AverageTimeDomProcessing(),
+ new AverageTimeDomCompletion(),
+ new AverageTimeOnLoad(),
+ new AveragePageLoadTime()
+ ];
+
+ $mappedMetrics = [];
+
+ foreach ($metrics as $metric) {
+ $mappedMetrics[$metric->getName()] = $metric;
+ }
+
+ return $mappedMetrics;
+ }
+
+ public static function getMetricTranslations()
+ {
+ $translations = array();
+ foreach (self::getAllPagePerformanceMetrics() as $metric) {
+ $translations[$metric->getName()] = $metric->getTranslatedName();
+ }
+
+ return $translations;
+ }
+
+ public static function attachActionMetrics(&$metricsConfig)
+ {
+ /**
+ * @var ActionDimension[] $performanceDimensions
+ */
+ $performanceDimensions = [
+ new TimeNetwork(),
+ new TimeServer(),
+ new TimeTransfer(),
+ new TimeDomProcessing(),
+ new TimeDomCompletion(),
+ new TimeOnLoad()
+ ];
+ foreach($performanceDimensions as $dimension) {
+ $id = $dimension->getColumnName();
+ $metricsConfig['sum_'.$id] = [
+ 'aggregation' => 'sum',
+ 'query' => "sum(
+ case when " . $id . " is null
+ then 0
+ else " . $id . "
+ end
+ ) / 1000"
+ ];
+ $metricsConfig['nb_hits_with_'.$id] = [
+ 'aggregation' => 'sum',
+ 'query' => "sum(
+ case when " . $id . " is null
+ then 0
+ else 1
+ end
+ )"
+ ];
+ $metricsConfig['min_'.$id] = [
+ 'aggregation' => 'min',
+ 'query' => "min(" . $id . ") / 1000"
+ ];
+ $metricsConfig['max_'.$id] = [
+ 'aggregation' => 'max',
+ 'query' => "max(" . $id . ") / 1000"
+ ];
+ }
+
+ return $metricsConfig;
+ }
+
+}
diff --git a/plugins/PagePerformance/PagePerformance.php b/plugins/PagePerformance/PagePerformance.php
new file mode 100644
index 0000000000..8655f92472
--- /dev/null
+++ b/plugins/PagePerformance/PagePerformance.php
@@ -0,0 +1,157 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\PagePerformance;
+
+use Piwik\DataTable;
+use Piwik\Plugin\ViewDataTable;
+use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable;
+
+/**
+ */
+class PagePerformance extends \Piwik\Plugin
+{
+ public static $availableForMethods = [
+ 'getPageUrls',
+ 'getEntryPageUrls',
+ 'getExitPageUrls',
+ 'getPageUrlsFollowingSiteSearch',
+ 'getPageTitles',
+ 'getPageTitlesFollowingSiteSearch',
+ ];
+
+ public function isTrackerPlugin()
+ {
+ return true;
+ }
+
+ /**
+ * @see \Piwik\Plugin::registerEvents
+ */
+ public function registerEvents()
+ {
+ return [
+ 'AssetManager.getJavaScriptFiles' => 'getJsFiles',
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ 'Actions.Archiving.addActionMetrics' => 'addActionMetrics',
+ 'ScheduledReports.processReports' => 'processReports',
+ 'ViewDataTable.configure' => 'configureViewDataTable',
+ 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations',
+ 'Metrics.isLowerValueBetter' => 'isLowerValueBetter',
+ 'API.Request.dispatch.end' => 'enrichApi'
+ ];
+ }
+
+ public function getJsFiles(&$jsFiles)
+ {
+ $jsFiles[] = 'plugins/PagePerformance/javascripts/PagePerformance.js';
+ $jsFiles[] = 'plugins/PagePerformance/javascripts/rowaction.js';
+ $jsFiles[] = 'plugins/PagePerformance/javascripts/jqplotStackedBarEvolutionGraph.js';
+ }
+
+ public function getClientSideTranslationKeys(&$translationKeys)
+ {
+ $translationKeys[] = 'PagePerformance_RowActionTitle';
+ $translationKeys[] = 'PagePerformance_RowActionDescription';
+ $translationKeys[] = 'PagePerformance_PagePerformanceTitle';
+ $translationKeys[] = 'General_Total';
+ }
+
+ public function addMetricTranslations(&$translations)
+ {
+ $metrics = Metrics::getMetricTranslations();
+ $translations = array_merge($translations, $metrics);
+ }
+
+ public function isLowerValueBetter(&$isLowerBetter, $metric)
+ {
+ if (array_key_exists($metric, Metrics::getAllPagePerformanceMetrics())) {
+ $isLowerBetter = true;
+ }
+ }
+
+ public function enrichApi($dataTable, $params)
+ {
+ if ('Actions' !== $params['module'] || !$dataTable instanceof DataTable\DataTableInterface) {
+ return;
+ }
+
+ // remove additional metrics for action reports that don't have data
+ if (!in_array($params['action'], self::$availableForMethods)) {
+ $dataTable->deleteColumns([
+ 'sum_time_network',
+ 'nb_hits_with_time_network',
+ 'min_time_network',
+ 'max_time_network',
+ 'sum_time_server',
+ 'nb_hits_with_time_server',
+ 'min_time_server',
+ 'max_time_server',
+ 'sum_time_transfer',
+ 'nb_hits_with_time_transfer',
+ 'min_time_transfer',
+ 'max_time_transfer',
+ 'sum_time_dom_processing',
+ 'nb_hits_with_time_dom_processing',
+ 'min_time_dom_processing',
+ 'max_time_dom_processing',
+ 'sum_time_dom_completion',
+ 'nb_hits_with_time_dom_completion',
+ 'min_time_dom_completion',
+ 'max_time_dom_completion',
+ 'sum_time_on_load',
+ 'nb_hits_with_time_on_load',
+ 'min_time_on_load',
+ 'max_time_on_load',
+ ], true);
+ return;
+ }
+
+ $dataTable->filter(function (DataTable $dataTable) {
+ $extraProcessedMetrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME);
+
+ if (empty($extraProcessedMetrics)) {
+ $extraProcessedMetrics = array();
+ }
+
+ foreach (Metrics::getAllPagePerformanceMetrics() as $pagePerformanceMetric) {
+ $extraProcessedMetrics[] = $pagePerformanceMetric;
+ }
+
+ $dataTable->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics);
+ });
+ }
+
+ public function configureViewDataTable(ViewDataTable $view)
+ {
+ $module = $view->requestConfig->getApiModuleToRequest();
+ $method = $view->requestConfig->getApiMethodToRequest();
+ if ('Actions' === $module && in_array($method, self::$availableForMethods) && $view instanceof HtmlTable) {
+ $view->config->columns_to_display[] = 'avg_page_load_time';
+ }
+ }
+
+ public function addActionMetrics(&$metricsConfig)
+ {
+ Metrics::attachActionMetrics($metricsConfig);
+ }
+
+ public function processReports(&$processedReports, $reportType, $outputType, $report)
+ {
+ foreach ($processedReports as &$processedReport) {
+ $metadata = &$processedReport['metadata'];
+
+ // Ensure average page load time is displayed in the evolution chart
+ if ($metadata['module'] == 'PagePerformance') {
+ $metadata['imageGraphUrl'] .= '&columns=avg_page_load_time';
+ $metadata['imageGraphEvolutionUrl'] .= '&columns=avg_page_load_time';
+ }
+ }
+ }
+}
diff --git a/plugins/PagePerformance/Reports/Get.php b/plugins/PagePerformance/Reports/Get.php
new file mode 100644
index 0000000000..37f463445f
--- /dev/null
+++ b/plugins/PagePerformance/Reports/Get.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\PagePerformance\Reports;
+
+use Piwik\DataTable;
+use Piwik\Metrics\Formatter;
+use Piwik\Piwik;
+use Piwik\Plugin\ViewDataTable;
+use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines;
+use Piwik\Plugins\PagePerformance\Metrics;
+use Piwik\Plugins\PagePerformance\Visualizations\JqplotGraph\StackedBarEvolution;
+use Piwik\Report\ReportWidgetFactory;
+use Piwik\Widget\WidgetsList;
+
+class Get extends \Piwik\Plugin\Report
+{
+ protected function init()
+ {
+ parent::init();
+
+ $this->dimension = null;
+ $this->categoryId = 'General_Visitors';
+ $this->subcategoryId = 'General_Overview';
+
+ $this->name = Piwik::translate('PagePerformance_Overview');
+ $this->documentation = '';
+ $this->onlineGuideUrl = 'https://matomo.org/docs/page-performance/';
+ $this->processedMetrics = [
+ // none
+ ];
+ $this->metrics = Metrics::getAllPagePerformanceMetrics();
+ }
+
+ public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)
+ {
+ $config = $factory->createWidget();
+ $config->forceViewDataTable(StackedBarEvolution::ID);
+ $config->setAction('getEvolutionGraph');
+ $config->setOrder(20);
+ $config->setName('PagePerformance_EvolutionOverPeriod');
+ $widgetsList->addWidgetConfig($config);
+
+ $config = $factory->createWidget();
+ $config->forceViewDataTable(Sparklines::ID);
+ $config->setName('');
+ $config->setIsNotWidgetizable();
+ $config->setOrder(21);
+ $widgetsList->addWidgetConfig($config);
+ }
+
+ public function configureView(ViewDataTable $view)
+ {
+ if ($view->isViewDataTableId(Sparklines::ID)
+ && $view instanceof Sparklines
+ ) {
+ $this->addSparklineColumns($view);
+
+ $numberFormatter = new Formatter\Html();
+ $metrics = $this->getMetrics();
+ $view->config->filters[] = function (DataTable $table) use ($numberFormatter, $metrics) {
+ $firstRow = $table->getFirstRow();
+ if ($firstRow) {
+ foreach ($metrics as $metric => $name) {
+ $metricValue = $firstRow->getColumn($metric);
+ if (false !== $metricValue) {
+ $firstRow->setColumn($metric, $numberFormatter->getPrettyTimeFromSeconds($metricValue));
+ }
+ }
+ }
+ };
+
+ $view->config->columns_to_display = array_keys(Metrics::getAllPagePerformanceMetrics());
+ $view->config->setNotLinkableWithAnyEvolutionGraph();
+ }
+ }
+
+ private function addSparklineColumns(Sparklines $view)
+ {
+ $count = 0;
+ foreach ($this->getMetrics() as $metric => $translation) {
+ $view->config->addSparklineMetric([$metric], $count++);
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/Tracker/PerformanceDataProcessor.php b/plugins/PagePerformance/Tracker/PerformanceDataProcessor.php
new file mode 100644
index 0000000000..54471db6ea
--- /dev/null
+++ b/plugins/PagePerformance/Tracker/PerformanceDataProcessor.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\Tracker;
+
+use Piwik\Common;
+use Piwik\Log;
+use Piwik\Plugin\Dimension\ActionDimension;
+use Piwik\Plugins\PagePerformance\Columns\TimeDomCompletion;
+use Piwik\Plugins\PagePerformance\Columns\TimeDomProcessing;
+use Piwik\Plugins\PagePerformance\Columns\TimeNetwork;
+use Piwik\Plugins\PagePerformance\Columns\TimeServer;
+use Piwik\Plugins\PagePerformance\Columns\TimeOnLoad;
+use Piwik\Plugins\PagePerformance\Columns\TimeTransfer;
+use Piwik\Tracker;
+use Piwik\Tracker\Action;
+use Piwik\Tracker\ActionPageview;
+use Piwik\Tracker\Model;
+use Piwik\Tracker\Request;
+use Piwik\Tracker\RequestProcessor;
+use Piwik\Tracker\Visit\VisitProperties;
+
+/**
+ * Handles tracker requests containing performance data.
+ */
+class PerformanceDataProcessor extends RequestProcessor
+{
+ public function recordLogs(VisitProperties $visitProperties, Request $request)
+ {
+ /** @var null|Action $action */
+ $action = $request->getMetadata('Actions', 'action');
+
+ // update performance metrics only for non pageview requests
+ if ($action && $action instanceof ActionPageview) {
+ return;
+ }
+
+ $pageViewId = $request->getParam('pv_id');
+ $idVisit = $visitProperties->getProperty('idvisit');
+
+ // ignore requests that can't be associated with an existing page view of a visitor
+ if (empty($pageViewId) || empty($idVisit)) {
+ return;
+ }
+
+ $idLinkVa = $this->getPageViewId($idVisit, $pageViewId);
+
+ // ignore requests
+ if (empty($idLinkVa)) {
+ return;
+ }
+
+ /** @var ActionDimension[] $performanceDimensions */
+ $performanceDimensions = [
+ new TimeNetwork(),
+ new TimeServer(),
+ new TimeTransfer(),
+ new TimeDomProcessing(),
+ new TimeDomCompletion(),
+ new TimeOnLoad()
+ ];
+
+ $valuesToUpdate = [];
+
+ foreach ($performanceDimensions as $performanceDimension) {
+ $paramValue = $request->getParam($performanceDimension->getRequestParam());
+ if ($paramValue > -1) {
+ $valuesToUpdate[$performanceDimension->getColumnName()] = $paramValue;
+ }
+ }
+
+ if (empty($valuesToUpdate)) {
+ return; // no values to update given with the request
+ }
+
+ Log::info('Updating page performance metrics of page view with id ' . $pageViewId);
+
+ $model = new Model();
+ $model->updateAction($idLinkVa, $valuesToUpdate);
+ }
+
+ protected function getPageViewId($idVisit, $pageViewId)
+ {
+ $query = sprintf(
+ 'SELECT idlink_va FROM %1$s LEFT JOIN %2$s ON idaction_url = idaction WHERE idvisit = ? AND idpageview = ? AND %2$s.type = ?',
+ Common::prefixTable('log_link_visit_action'),
+ Common::prefixTable('log_action')
+ );
+
+ $idLinkVa = Tracker::getDatabase()->fetchOne($query, [$idVisit, $pageViewId, Action::TYPE_PAGE_URL]);
+
+ if (!empty($idLinkVa)) {
+ return $idLinkVa;
+ }
+
+ $query = sprintf(
+ 'SELECT idlink_va FROM %1$s LEFT JOIN %2$s ON idaction_name = idaction WHERE idvisit = ? AND idpageview = ? AND %2$s.type = ?',
+ Common::prefixTable('log_link_visit_action'),
+ Common::prefixTable('log_action')
+ );
+
+ return Tracker::getDatabase()->fetchOne($query, [$idVisit, $pageViewId, Action::TYPE_PAGE_TITLE]);
+ }
+}
diff --git a/plugins/PagePerformance/Visualizations/JqplotGraph/StackedBarEvolution.php b/plugins/PagePerformance/Visualizations/JqplotGraph/StackedBarEvolution.php
new file mode 100644
index 0000000000..1ffd6c2ec5
--- /dev/null
+++ b/plugins/PagePerformance/Visualizations/JqplotGraph/StackedBarEvolution.php
@@ -0,0 +1,99 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\PagePerformance\Visualizations\JqplotGraph;
+
+use Piwik\Common;
+use Piwik\Period\Range;
+use Piwik\Plugins\PagePerformance\JqplotDataGenerator;
+use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution;
+use Piwik\Site;
+
+/**
+ * Visualization that renders HTML for a line graph using jqPlot.
+ *
+ * @property Evolution\Config $config
+ */
+class StackedBarEvolution extends Evolution
+{
+ const ID = 'graphStackedBarEvolution';
+ const FOOTER_ICON_TITLE = '';
+ const FOOTER_ICON = '';
+
+ public function beforeRender()
+ {
+ parent::beforeRender();
+
+ $this->checkRequestIsOnlyForMultiplePeriods();
+
+ $this->config->show_flatten_table = false;
+ $this->config->show_limit_control = false;
+ $this->config->datatable_js_type = 'JqplotStackedBarEvolutionGraphDataTable';
+ }
+
+ public function beforeLoadDataTable()
+ {
+ $this->calculateEvolutionDateRange();
+
+ parent::beforeLoadDataTable();
+
+ // period will be overridden when 'range' is requested in the UI
+ // but the graph will display for each day of the range.
+ // Default 'range' behavior is to return the 'sum' for the range
+ if (Common::getRequestVar('period', false) == 'range') {
+ $this->requestConfig->request_parameters_to_modify['period'] = 'day';
+ }
+
+ $this->config->custom_parameters['columns'] = $this->config->columns_to_display;
+ }
+
+ protected function makeDataGenerator($properties)
+ {
+ return new JqplotDataGenerator\StackedBarEvolution($properties, 'evolution', $this);
+ }
+
+ /**
+ * Based on the period, date and evolution_{$period}_last_n query parameters,
+ * calculates the date range this evolution chart will display data for.
+ */
+ private function calculateEvolutionDateRange()
+ {
+ $period = Common::getRequestVar('period');
+ $idSite = Common::getRequestVar('idSite');
+ $timezone = Site::getTimezoneFor($idSite);
+
+ $defaultLastN = self::getDefaultLastN($period);
+ $originalDate = Common::getRequestVar('date', 'last' . $defaultLastN, 'string');
+
+ if ('range' != $period) { // show evolution limit if the period is not a range
+ // set the evolution_{$period}_last_n query param
+ if (Range::parseDateRange($originalDate)) {
+ // if a multiple period
+
+ // overwrite last_n param using the date range
+ $oPeriod = new Range($period, $originalDate, $timezone);
+ $lastN = count($oPeriod->getSubperiods());
+
+ } else {
+ // if not a multiple period
+ list($newDate, $lastN) = self::getDateRangeAndLastN($period, $originalDate, $defaultLastN);
+ $this->requestConfig->request_parameters_to_modify['date'] = $newDate;
+ $this->config->custom_parameters['dateUsedInGraph'] = $newDate;
+ }
+
+ $lastNParamName = self::getLastNParamName($period);
+ $this->config->custom_parameters[$lastNParamName] = $lastN;
+ }
+ }
+
+ public function supportsComparison()
+ {
+ return false;
+ }
+}
diff --git a/plugins/PagePerformance/Visualizations/PerformanceColumns.php b/plugins/PagePerformance/Visualizations/PerformanceColumns.php
new file mode 100644
index 0000000000..98298bc6f8
--- /dev/null
+++ b/plugins/PagePerformance/Visualizations/PerformanceColumns.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\PagePerformance\Visualizations;
+
+use Piwik\DataTable;
+use Piwik\DbHelper;
+use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable;
+use Piwik\Plugins\PagePerformance\Metrics;
+use Piwik\Plugins\PagePerformance\PagePerformance;
+
+/**
+ * DataTable Visualization that derives from HtmlTable and show performance columns.
+ */
+class PerformanceColumns extends HtmlTable
+{
+ const ID = 'tablePerformanceColumns';
+ const FOOTER_ICON = 'icon-page-performance';
+ const FOOTER_ICON_TITLE = 'PagePerformance_PerformanceTable';
+
+ public function beforeRender()
+ {
+ parent::beforeRender();
+ }
+
+ public static function canDisplayViewDataTable($viewDataTable)
+ {
+ $request = $viewDataTable->getRequestArray();
+
+ if (is_array($request) && array_key_exists('module', $request) && array_key_exists('action', $request) &&
+ 'Actions' === $request['module'] && in_array($request['action'], PagePerformance::$availableForMethods)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function beforeGenericFiltersAreAppliedToLoadedDataTable()
+ {
+ $this->config->datatable_css_class = 'dataTableVizAllColumns';
+
+ $properties = $this->config;
+
+ $this->dataTable->filter(function (DataTable $dataTable) use ($properties) {
+ $properties->columns_to_display = array_merge([
+ 'label',
+ 'nb_visits',
+ ], array_keys(Metrics::getAllPagePerformanceMetrics()));
+
+ if (version_compare(DbHelper::getInstallVersion(),'4.0.0-b1', '<')) {
+ $properties->columns_to_display[] = 'avg_time_generation';
+ }
+ });
+
+ parent::beforeGenericFiltersAreAppliedToLoadedDataTable();
+ }
+
+ public function beforeLoadDataTable()
+ {
+ parent::beforeLoadDataTable();
+
+ unset($this->requestConfig->request_parameters_to_modify['pivotBy']);
+ unset($this->requestConfig->request_parameters_to_modify['pivotByColumn']);
+ }
+
+ protected function isPivoted()
+ {
+ return false; // Pivot not supported
+ }
+}
diff --git a/plugins/PagePerformance/javascripts/PagePerformance.js b/plugins/PagePerformance/javascripts/PagePerformance.js
new file mode 100644
index 0000000000..015671172a
--- /dev/null
+++ b/plugins/PagePerformance/javascripts/PagePerformance.js
@@ -0,0 +1,141 @@
+var PagePerformance = function() {
+
+ function getDataTableFromApiMethod(apiMethod)
+ {
+ var div = $(require('piwik/UI').DataTable.getDataTableByReport(apiMethod));
+ if (div.length && div.data('uiControlObject')) {
+ return div.data('uiControlObject');
+ }
+ }
+
+ function getLabelFromTr ($tr, apiMethod) {
+ var label;
+
+ if (apiMethod && 0 === apiMethod.indexOf('Actions.')) {
+ // for now only use this for Actions... I know a hack :( Otherwise in Search Engines
+ // it would show "http://www.searchenginename.org" instead of "SearchEngineName"
+ label = $tr.attr('data-url-label');
+ }
+
+ if (!label) {
+ label = $tr.find('.label .value').text();
+ }
+
+ if (label) {
+ label = $.trim(label);
+ }
+
+ return label;
+ }
+
+
+ function getDimensionFromApiMethod(apiMethod)
+ {
+ if (!apiMethod) {
+ return;
+ }
+
+ var dataTable = getDataTableFromApiMethod(apiMethod);
+ var metadata = getMetadataFromDataTable(dataTable);
+
+ if (metadata && metadata.dimension) {
+ return metadata.dimension;
+ }
+ }
+
+ function getMetadataFromDataTable(dataTable)
+ {
+ if (dataTable) {
+
+ return dataTable.getReportMetadata();
+ }
+ }
+
+ function findTitleOfRowHavingRawSegmentValue(apiMethod, rawSegmentValue)
+ {
+ var $tr = $('[data-report="' + apiMethod + '"] tr[data-segment-filter="' + rawSegmentValue + '"]').first();
+
+ return getLabelFromTr($tr, apiMethod);
+ }
+
+ function setPopoverTitle(apiMethod, label, index) {
+ var dataTable = getDataTableFromApiMethod(apiMethod);
+
+ if (!dataTable) {
+ if (index < 15) {
+ // this is needed when the popover is opened before the dataTable is there which can often
+ // happen when opening the popover directly via URL (broadcast.popoverHandler)
+ setTimeout(function () {
+ setPopoverTitle(apiMethod, label, index + 1);
+ }, 150);
+ }
+ return;
+ }
+
+ var type = getDimensionFromApiMethod(apiMethod);
+
+ var separator = ' > '; // LabelFilter::SEPARATOR_RECURSIVE_LABEL
+ var labelParts = label.split(separator);
+ for (var i = 0; i < labelParts.length; i++) {
+ var labelPart = labelParts[i].replace('@', '');
+ labelParts[i] = $.trim(decodeURIComponent(labelPart));
+ }
+ var delimiter = piwik.config.action_url_category_delimiter;
+ if(apiMethod.indexOf('PageTitles') >= 0) {
+ delimiter = piwik.config.action_title_category_delimiter;
+ }
+ label = labelParts.join(delimiter);
+
+ // encode label for usage in .html()
+ label = piwikHelper.htmlEntities(label);
+ label = piwikHelper.escape(label);
+ label = label.replace(/(&amp;)(#[0-9]{2,5};)/g, '&$2');
+
+ var title = _pk_translate('PagePerformance_PagePerformanceTitle', [type, label]);
+
+ Piwik_Popover.setTitle(title);
+ }
+
+ function show(apiMethod, label) {
+
+ // open the popover
+ var box = Piwik_Popover.showLoading('Page performance report');
+ box.addClass('pagePerformancePopover');
+
+
+ var callback = function (html) {
+ Piwik_Popover.setContent(html);
+
+ // remove title returned from the server
+ var title = box.find('h2[piwik-enriched-headline]');
+ var defaultTitle = title.text();
+
+ if (title.length) {
+ title.remove();
+ }
+
+ Piwik_Popover.setTitle(defaultTitle);
+
+ setPopoverTitle(apiMethod, label, 0);
+ };
+
+ // prepare loading the popover contents
+ var requestParams = {
+ module: 'PagePerformance',
+ action: 'indexPagePerformance',
+ apiMethod: apiMethod,
+ label: encodeURIComponent(label),
+ };
+
+ var ajaxRequest = new ajaxHelper();
+ ajaxRequest.addParams(requestParams, 'get');
+ ajaxRequest.setCallback(callback);
+ ajaxRequest.setFormat('html');
+ ajaxRequest.send();
+ }
+
+ return {
+ show: show
+ }
+}();
+
diff --git a/plugins/PagePerformance/javascripts/jqplotStackedBarEvolutionGraph.js b/plugins/PagePerformance/javascripts/jqplotStackedBarEvolutionGraph.js
new file mode 100644
index 0000000000..138f9b4653
--- /dev/null
+++ b/plugins/PagePerformance/javascripts/jqplotStackedBarEvolutionGraph.js
@@ -0,0 +1,218 @@
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * DataTable UI class for JqplotGraph/StackedBarEvolution.
+ *
+ * @link http://www.jqplot.com
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+(function ($, require) {
+
+ var exports = require('piwik/UI'),
+ JqplotGraphDataTable = exports.JqplotGraphDataTable,
+ JqplotGraphDataTablePrototype = JqplotGraphDataTable.prototype;
+
+ exports.JqplotStackedBarEvolutionGraphDataTable = function (element) {
+ JqplotGraphDataTable.call(this, element);
+ };
+
+ $.extend(exports.JqplotStackedBarEvolutionGraphDataTable.prototype, JqplotGraphDataTablePrototype, {
+
+ _setJqplotParameters: function (params) {
+ JqplotGraphDataTablePrototype._setJqplotParameters.call(this, params);
+
+ var defaultParams = {
+ axes: {
+ xaxis: {
+ pad: 1.0,
+ renderer: $.jqplot.CategoryAxisRenderer,
+ tickOptions: {
+ showGridline: false
+ }
+ }
+ },
+ piwikTicks: {
+ showTicks: true,
+ showGrid: true,
+ showHighlight: false,
+ tickColor: this.tickColor
+ }
+ };
+
+ defaultParams.seriesDefaults = {
+ renderer: $.jqplot.BarRenderer,
+ rendererOptions: {
+ shadowOffset: 1,
+ shadowDepth: 2,
+ shadowAlpha: .2,
+ fillToZero: true,
+ barMargin: this.data[0].length > 10 ? 2 : 10
+ }
+ };
+
+ defaultParams.stackSeries = true;
+
+ var overrideParams = {
+ legend: {
+ show: false
+ },
+ canvasLegend: {
+ show: true
+ }
+ };
+ this.jqplotParams = $.extend(true, {}, defaultParams, this.jqplotParams, overrideParams);
+ },
+
+ _bindEvents: function () {
+ JqplotGraphDataTablePrototype._bindEvents.call(this);
+
+ var self = this;
+ var lastTick = false;
+
+ $('#' + this.targetDivId)
+ .on('jqplotMouseLeave', function (e, s, i, d) {
+ $(this).css('cursor', 'default');
+ JqplotGraphDataTablePrototype._destroyDataPointTooltip.call(this, $(this));
+ })
+ .on('jqplotClick', function (e, s, i, d) {
+ if (lastTick !== false && typeof self.jqplotParams.axes.xaxis.onclick != 'undefined'
+ && typeof self.jqplotParams.axes.xaxis.onclick[lastTick] == 'string') {
+ var url = self.jqplotParams.axes.xaxis.onclick[lastTick];
+
+ broadcast.propagateNewPage(url);
+ }
+ })
+ .on('jqplotPiwikTickOver', function (e, tick) {
+ lastTick = tick;
+ var label;
+
+ var dataByAxis = {};
+ var totalValue = 0;
+ for (var d = 0; d < self.data.length; ++d) {
+ var valueUnformatted = self.data[d][tick];
+ if (typeof valueUnformatted === 'undefined' || valueUnformatted === null) {
+ continue;
+ }
+
+ totalValue += valueUnformatted;
+
+ var axis = self.jqplotParams.series[d]._xaxis || 'xaxis';
+ if (!dataByAxis[axis]) {
+ dataByAxis[axis] = [];
+ }
+
+ var value = self.formatY(valueUnformatted, d);
+ var series = self.jqplotParams.series[d].label;
+
+ var seriesColor = self.jqplotParams.seriesColors[d];
+
+ dataByAxis[axis].push('<span class="tooltip-series-color" style="background-color: ' + seriesColor + ';"/>' + '<strong>' + value + '</strong> ' + piwikHelper.htmlEntities(series));
+ }
+
+ dataByAxis[axis].push('<span class="tooltip-series-color" style="background-color: #000;"/>' + '<strong>' + self.formatY(totalValue, 0) + '</strong> ' + _pk_translate('General_Total'));
+
+
+ var xAxisCount = 0;
+ Object.keys(self.jqplotParams.axes).forEach(function (axis) {
+ if (axis.substring(0, 1) === 'x') {
+ ++xAxisCount;
+ }
+ });
+
+ var content = '';
+ for (var i = 0; i < xAxisCount; ++i) {
+ var axisName = i === 0 ? 'xaxis' : 'x' + (i + 1) + 'axis';
+ if (!dataByAxis[axisName] || !dataByAxis[axisName].length) {
+ continue;
+ }
+
+ if (typeof self.jqplotParams.axes[axisName].labels != 'undefined') {
+ label = self.jqplotParams.axes[axisName].labels[tick];
+ } else {
+ label = self.jqplotParams.axes[axisName].ticks[tick];
+ }
+
+ if (typeof label === 'undefined') { // sanity check
+ continue;
+ }
+
+ content += '<h3 class="evolution-tooltip-header">'+piwikHelper.htmlEntities(label)+'</h3>'+dataByAxis[axisName].join('<br />');
+ }
+
+ $(this).tooltip({
+ track: true,
+ items: 'div',
+ content: content,
+ show: false,
+ hide: false
+ }).trigger('mouseover');
+ if (typeof self.jqplotParams.axes.xaxis.onclick != 'undefined'
+ && typeof self.jqplotParams.axes.xaxis.onclick[lastTick] == 'string') {
+ $(this).css('cursor', 'pointer');
+ }
+ });
+
+ this.setYTicks();
+ },
+
+ _destroyDataPointTooltip: function () {
+ // do nothing, tooltips are destroyed in the jqplotMouseLeave event
+ },
+
+ render: function () {
+ JqplotGraphDataTablePrototype.render.call(this);
+
+ if (initializeSparklines) {
+ initializeSparklines();
+ }
+ },
+
+ setYTicksForAxis: function (axisName, axis) {
+ // calculate maximum x value of all data sets
+ var maxCrossDataSets = 0;
+
+ for (var j = 0; j < this.data[0].length; j++) {
+ var sum = 0;
+ for (var i = 0; i < this.data.length; i++) {
+ if (this.jqplotParams.series[i].yaxis == axisName) {
+ sum += this.data[i][j];
+ }
+ }
+ sum = parseFloat(sum);
+ if (sum > maxCrossDataSets) {
+ maxCrossDataSets = sum;
+ }
+ }
+
+ // add little padding on top
+ maxCrossDataSets += Math.round(maxCrossDataSets * .03);
+
+ // round to the nearest multiple of ten
+ if (maxCrossDataSets > 15) {
+ maxCrossDataSets = maxCrossDataSets + 10 - maxCrossDataSets % 10;
+ }
+
+ if (maxCrossDataSets == 0) {
+ maxCrossDataSets = 1;
+ }
+
+ // make sure percent axes don't go above 100%
+ if (axis.tickOptions.formatString.substring(2, 3) == '%' && maxCrossDataSets > 100) {
+ maxCrossDataSets = 100;
+ }
+
+ // calculate y-values for ticks
+ var ticks = [];
+ var numberOfTicks = 2;
+ var tickDistance = Math.ceil(maxCrossDataSets / numberOfTicks);
+ for (var i = 0; i <= numberOfTicks; i++) {
+ ticks.push(i * tickDistance);
+ }
+ axis.ticks = ticks;
+ },
+
+ });
+
+})(jQuery, require); \ No newline at end of file
diff --git a/plugins/PagePerformance/javascripts/rowaction.js b/plugins/PagePerformance/javascripts/rowaction.js
new file mode 100644
index 0000000000..df7dbca108
--- /dev/null
+++ b/plugins/PagePerformance/javascripts/rowaction.js
@@ -0,0 +1,103 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * This file registers the page performance row action on the pages report.
+ */
+
+(function () {
+
+ var actionName = 'PagePerformance';
+
+ function getDataTableFromApiMethod(apiMethod)
+ {
+ var div = $(require('piwik/UI').DataTable.getDataTableByReport(apiMethod));
+ if (div.length && div.data('uiControlObject')) {
+ return div.data('uiControlObject');
+ }
+ }
+
+ function DataTable_RowActions_PagePerformance(dataTable) {
+ this.dataTable = dataTable;
+ this.actionName = actionName;
+
+ // has to be overridden in subclasses
+ this.trEventName = 'matomoTriggerPagePerformanceAction';
+ }
+
+ DataTable_RowActions_PagePerformance.prototype = new DataTable_RowAction();
+
+ DataTable_RowActions_PagePerformance.prototype.performAction = function (label, tr, e, originalRow) {
+ var apiMethod = this.dataTable.param.module + '.' + this.dataTable.param.action;
+ this.openPopover(apiMethod, label);
+ };
+
+ DataTable_RowActions_PagePerformance.prototype.openPopover = function (apiMethod, label) {
+ var urlParam = apiMethod + ':' + label;
+ DataTable_RowAction.prototype.openPopover.apply(this, [urlParam]);
+ };
+
+ DataTable_RowActions_PagePerformance.prototype.doOpenPopover = function (urlParam) {
+ var urlParamParts = urlParam.split(':');
+ var apiMethod = urlParamParts.shift();
+ var label = decodeURIComponent(urlParamParts.shift());
+
+ PagePerformance.show(apiMethod, label);
+ };
+
+ DataTable_RowActions_Registry.register({
+
+ name: actionName,
+
+ dataTableIcon: 'icon-page-performance',
+
+ order: 50,
+
+ dataTableIconTooltip: [
+ _pk_translate('PagePerformance_RowActionTitle'),
+ _pk_translate('PagePerformance_RowActionDescription')
+ ],
+
+ isAvailableOnReport: function (dataTableParams) {
+ return dataTableParams.module == 'Actions'
+ && (dataTableParams.action == 'getPageUrls' || dataTableParams.action == 'getEntryPageUrls' ||
+ dataTableParams.action == 'getExitPageUrls' || dataTableParams.action == 'getPageUrlsFollowingSiteSearch' ||
+ dataTableParams.action == 'getPageTitles' || dataTableParams.action == 'getPageTitlesFollowingSiteSearch');
+ },
+
+ isAvailableOnRow: function (dataTableParams, tr) {
+ return true;
+ },
+
+ createInstance: function (dataTable, param) {
+ if (dataTable !== null && typeof dataTable.pagePerformanceInstance != 'undefined') {
+ return dataTable.pagePerformanceInstance;
+ }
+
+ if (dataTable === null && param) {
+ // when row evolution is triggered from the url (not a click on the data table)
+ // we look for the data table instance in the dom
+ var report = param.split(':')[0];
+ var div = $(require('piwik/UI').DataTable.getDataTableByReport(report));
+ if (div.length && div.data('uiControlObject')) {
+ dataTable = div.data('uiControlObject');
+ if (typeof dataTable.pagePerformanceInstance != 'undefined') {
+ return dataTable.pagePerformanceInstance;
+ }
+ }
+ }
+
+ var instance = new DataTable_RowActions_PagePerformance(dataTable);
+ if (dataTable !== null) {
+ dataTable.pagePerformanceInstance = instance;
+ }
+ return instance;
+ },
+
+ });
+
+})(); \ No newline at end of file
diff --git a/plugins/PagePerformance/lang/en.json b/plugins/PagePerformance/lang/en.json
new file mode 100644
index 0000000000..3aa08ad71e
--- /dev/null
+++ b/plugins/PagePerformance/lang/en.json
@@ -0,0 +1,39 @@
+{
+ "PagePerformance": {
+ "ColumnAveragePageLoadTime": "Avg. page load time",
+ "ColumnAveragePageLoadTimeDocumentation": "Average time (in seconds) how long it takes from requesting a page until the page is fully rendered within the browser",
+ "ColumnAverageTimeNetwork": "Avg. network time",
+ "ColumnAverageTimeNetworkDocumentation": "Average time (in seconds) how long it takes to connect to server. This includes the time needed to lookup DNS and establish a TCP connection. This value might be 0 after the first request to a domain as the browser might cache the connection.",
+ "ColumnAverageTimeServer": "Avg. server time",
+ "ColumnAverageTimeServerDocumentation": "Average time (in seconds) how long it takes the server to generate page. This is the time between the server receiving the request and start serving the response.",
+ "ColumnAverageTimeTransfer": "Avg. transfer time",
+ "ColumnAverageTimeTransferDocumentation": "Average time (in seconds) how long it takes the browser to download the response from the server. This is the time from receiving the first byte till the response is complete.",
+ "ColumnAverageTimeDomProcessing": "Avg. DOM processing time",
+ "ColumnAverageTimeDomProcessingDocumentation": "Average time (in seconds) how long the browser spends loading the webpage after the response was fully received until the user can starting interacting with it.",
+ "ColumnAverageTimeDomCompletion": "Avg. DOM completion time",
+ "ColumnAverageTimeDomCompletionDocumentation": "Average time (in seconds) how long it takes for the browser to load media and execute any Javascript code listening for the DOMContentLoaded event after the the webpage was loaded and the user can already interact with it.",
+ "ColumnAverageTimeOnLoad": "Avg. on load time",
+ "ColumnAverageTimeOnLoadDocumentation": "Average time (in seconds) how long it takes the browser to execute Javascript code waiting for the window.load event. This event is triggered once the DOM was completely rendered.",
+ "ColumnViewsWithTimeNetwork": "Pageviews with network time",
+ "ColumnViewsWithTimeServer": "Pageviews with server time",
+ "ColumnViewsWithTimeTransfer": "Pageviews with transfer time",
+ "ColumnViewsWithTimeDomProcessing": "Pageviews with DOM processing time",
+ "ColumnViewsWithTimeDomCompletion": "Pageviews with DOM completion time",
+ "ColumnViewsWithTimeOnLoad": "Pageviews with on load time",
+ "ColumnTimeNetwork": "Network time",
+ "ColumnTimeServer": "Server time",
+ "ColumnTimeTransfer": "Transfer time",
+ "ColumnTimeDomProcessing": "DOM processing time",
+ "ColumnTimeDomCompletion": "DOM completion time",
+ "ColumnTimeOnLoad": "On load time",
+ "PageLoadTime": "Page load time",
+ "EvolutionOverPeriod": "Evolution of page performance metrics",
+ "PluginDescription": "Adds some page performance reports",
+ "PerformanceTable": "Table with performance metrics",
+ "Overview": "Performance overview",
+ "HelpNote": "Some of those metrics might not always be available. You can find more information in our %1$sonline guide%2$s.",
+ "RowActionTitle": "Open page performance report",
+ "RowActionDescription": "Show page performance report for this row",
+ "PagePerformanceTitle": "Page performance for page with %1$s \"%2$s\""
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/templates/getPagePerformancePopover.twig b/plugins/PagePerformance/templates/getPagePerformancePopover.twig
new file mode 100644
index 0000000000..480edfa6a2
--- /dev/null
+++ b/plugins/PagePerformance/templates/getPagePerformancePopover.twig
@@ -0,0 +1,10 @@
+<div class="page-performance">
+ <div class="graph">
+ {{ graph|raw }}
+ </div>
+ <div class="alert alert-info">
+ {{ metrics|raw }}
+ <br />
+ {{ 'General_Note'|translate }}: {{ 'PagePerformance_HelpNote'|translate('<a href="https://matomo.org/docs/page-performance/" rel="noreferrer noopener" target="_blank">', '</a>')|raw }}
+ </div>
+</div>
diff --git a/plugins/PagePerformance/tests/Fixtures/VisitsWithPagePerformanceMetrics.php b/plugins/PagePerformance/tests/Fixtures/VisitsWithPagePerformanceMetrics.php
new file mode 100644
index 0000000000..692a79edbf
--- /dev/null
+++ b/plugins/PagePerformance/tests/Fixtures/VisitsWithPagePerformanceMetrics.php
@@ -0,0 +1,118 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\PagePerformance\tests\Fixtures;
+
+use Piwik\Date;
+use Piwik\Tests\Framework\Fixture;
+
+/**
+ * Adds two sites and tracks several visits all in the past.
+ */
+class VisitsWithPagePerformanceMetrics extends Fixture
+{
+ public $dateTime = '2010-03-12 01:22:33';
+ public $idSite = 1;
+
+ public function setUp(): void
+ {
+ $this->setUpWebsitesAndGoals();
+ $this->trackVisits();
+ }
+
+ public function tearDown(): void
+ {
+ // empty
+ }
+
+ public function setUpWebsitesAndGoals()
+ {
+ if (!self::siteCreated($idSite = 1)) {
+ self::createWebsite($this->dateTime);
+ }
+ }
+
+ protected function trackVisits()
+ {
+ $t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
+ $t->setUrl('http://example.org/category/Page1');
+ $t->setPerformanceTimings(12, 150, 333, 1101, 369, 150);
+ self::checkResponse($t->doTrackPageView('Page Title 1'));
+ $t->setUrl('http://example.org/Contact');
+ $t->setPerformanceTimings(0, 99, 298, 999, 412, 232);
+ self::checkResponse($t->doTrackPageView('Contact'));
+ $t->setUrl('http://example.org/Contact/ThankYou');
+ $t->setPerformanceTimings(36, 77, 412, 1055, 333, 77);
+ self::checkResponse($t->doTrackPageView('Contact'));
+
+ $dateTime = Date::factory($this->dateTime)->subDay(1)->addHour(2.6)->getDatetime();
+
+ $t = self::getTracker($this->idSite, $dateTime, $defaultInit = true);
+ $t->setUrl('http://example.org/category/Page1');
+ $t->setPerformanceTimings(12, 222, 211, 888, 299, 99);
+ self::checkResponse($t->doTrackPageView('Page Title 1'));
+ $t->setUrl('http://example.org/Contact');
+ $t->setPerformanceTimings(6, 99, 298, 1300, 348, 199);
+ self::checkResponse($t->doTrackPageView('Contact'));
+ $t->setUrl('http://example.org/Contact/ThankYou');
+ $t->setPerformanceTimings(36, 77, 412, 1140, 444, 120);
+ self::checkResponse($t->doTrackPageView('Contact'));
+
+ $dateTime = Date::factory($this->dateTime)->subDay(2)->addHour(6.5)->getDatetime();
+
+ $t = self::getTracker($this->idSite, $dateTime, $defaultInit = true);
+ $t->setUrl('http://example.org/category/Page1');
+ $t->setPerformanceTimings(29, 355, 444, 1300, 512, 333);
+ self::checkResponse($t->doTrackPageView('Page Title 1'));
+ $t->setUrl('http://example.org/Contact');
+ $t->setPerformanceTimings(66, 111, 278, 988, 355, 66);
+ self::checkResponse($t->doTrackPageView('Contact'));
+ $t->setUrl('http://example.org/Contact/ThankYou');
+ $t->setPerformanceTimings(23, 211, 399, 998, 355, 222);
+ self::checkResponse($t->doTrackPageView('Contact'));
+
+ $dateTime = Date::factory($this->dateTime)->subDay(3)->addHour(4.1)->getDatetime();
+
+ $t = self::getTracker($this->idSite, $dateTime, $defaultInit = true);
+ $t->setUrl('http://example.org/category/Page1');
+ $t->setPerformanceTimings(66, 277, 388, 1025, 436, 299);
+ self::checkResponse($t->doTrackPageView('Page Title 1'));
+ $t->setUrl('http://example.org/Contact');
+ $t->setPerformanceTimings(98, 99, 199, 899, 236, 100);
+ self::checkResponse($t->doTrackPageView('Contact'));
+ $t->setUrl('http://example.org/Contact/ThankYou');
+ $t->setPerformanceTimings(30, 123, 255, 1200, 233, 258);
+ self::checkResponse($t->doTrackPageView('Contact'));
+
+ $dateTime = Date::factory($this->dateTime)->subDay(4)->addHour(4.1)->getDatetime();
+
+ $t = self::getTracker($this->idSite, $dateTime, $defaultInit = true);
+ $t->setUrl('http://example.org/category/Page1');
+ $t->setPerformanceTimings(13, 158, 136, 1235, 359, 248);
+ self::checkResponse($t->doTrackPageView('Page Title 1'));
+ $t->setUrl('http://example.org/Contact');
+ $t->setPerformanceTimings(35, 132, 205, 1125, 236, 135);
+ self::checkResponse($t->doTrackPageView('Contact'));
+ $t->setUrl('http://example.org/Contact/ThankYou');
+ $t->setPerformanceTimings(40, 269, 195, 963, 195, 215);
+ self::checkResponse($t->doTrackPageView('Contact'));
+
+ $dateTime = Date::factory($this->dateTime)->subDay(15)->addHour(2.6)->getDatetime();
+
+ $t = self::getTracker($this->idSite, $dateTime, $defaultInit = true);
+ $t->setUrl('http://example.org/category/Page1');
+ $t->setPerformanceTimings(19, 222, 211, 888, 299, 99);
+ self::checkResponse($t->doTrackPageView('Page Title 1'));
+ $t->setUrl('http://example.org/Contact');
+ $t->setPerformanceTimings(22, 99, 298, 1300, 348, 199);
+ self::checkResponse($t->doTrackPageView('Contact'));
+ $t->setUrl('http://example.org/Contact/ThankYou');
+ $t->setPerformanceTimings(69, 77, 412, 1140, 444, 120);
+ self::checkResponse($t->doTrackPageView('Contact'));
+
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/Integration/Tracker/PerformanceDataProcessorTest.php b/plugins/PagePerformance/tests/Integration/Tracker/PerformanceDataProcessorTest.php
new file mode 100644
index 0000000000..da00b58045
--- /dev/null
+++ b/plugins/PagePerformance/tests/Integration/Tracker/PerformanceDataProcessorTest.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Matomo - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\PagePerformance\tests\Integration\Tracker;
+
+use Piwik\Common;
+use Piwik\Date;
+use Piwik\Db;
+use Piwik\Plugins\PagePerformance\Tracker\PerformanceDataProcessor;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+
+/**
+ * @group PagePerformance
+ * @group Plugins
+ */
+class PerformanceDataProcessorTest extends IntegrationTestCase
+{
+
+ /**
+ * @var PerformanceDataProcessor
+ */
+ private $requestProcessor;
+
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->requestProcessor = new PerformanceDataProcessor();
+
+ Fixture::createWebsite('2014-01-01 02:03:04');
+ }
+
+ public function test_shouldUpdatePerformanceTimingsInOngoingEventRequest()
+ {
+ $tracker = Fixture::getTracker(1, Date::now()->toString('Y-m-d H:i:s'));
+ $tracker->setUrl('http://example.org/test');
+ Fixture::checkResponse($tracker->doTrackPageView('My Page'));
+
+ $idPageView = $tracker->idPageview;
+
+ $this->checkActionHasTimings($idPageView);
+
+ $tracker->setPerformanceTimings(12, 77, 412, 1055, 333, 66);
+ Fixture::checkResponse($tracker->doTrackEvent('cat', 'act'));
+
+ $this->checkActionHasTimings($idPageView, 12, 77, 412, 1055, 333, 66);
+ }
+
+ public function test_shouldUpdatePerformanceTimingsInOngoingPingRequest()
+ {
+ $tracker = Fixture::getTracker(1, Date::now()->toString('Y-m-d H:i:s'));
+ $tracker->setUrl('http://example.org/test');
+ Fixture::checkResponse($tracker->doTrackPageView('My Page'));
+
+ $idPageView = $tracker->idPageview;
+
+ $this->checkActionHasTimings($idPageView);
+
+ $tracker->setPerformanceTimings(5, 66, 445, 1025, 12, 111);
+ Fixture::checkResponse($tracker->doPing());
+
+ $this->checkActionHasTimings($idPageView, 5, 66, 445, 1025, 12, 111);
+ }
+
+ public function test_shouldNotUpdatePerformanceTimingsInOngoingPageViewRequest()
+ {
+ $tracker = Fixture::getTracker(1, Date::now()->toString('Y-m-d H:i:s'));
+ $tracker->setUrl('http://example.org/test');
+ Fixture::checkResponse($tracker->doTrackPageView('My Page'));
+
+ $idPageView = $tracker->idPageview;
+
+ $this->checkActionHasTimings($idPageView);
+
+ $tracker->setPerformanceTimings(0, 66, 445, 1025, 12, 111);
+ $tracker->setUrl('http://example.org/test2');
+ Fixture::checkResponse($tracker->doTrackPageView('Another Page'));
+
+ $this->checkActionHasTimings($idPageView);
+ $this->checkActionHasTimings($tracker->idPageview, 0, 66, 445, 1025, 12, 111);
+ }
+
+ protected function checkActionHasTimings($pageViewId, $network = null, $server = null, $transfer = null, $domProcessing = null, $domCompletion = null, $onload = null)
+ {
+ $result = Db::fetchRow(
+ sprintf('SELECT time_network, time_server, time_transfer, time_dom_processing, time_dom_completion, time_on_load
+ FROM %1$s LEFT JOIN %2$s ON idaction_url = idaction WHERE idpageview = ? AND %2$s.type = 1',
+ Common::prefixTable('log_link_visit_action'),
+ Common::prefixTable('log_action')
+ ), $pageViewId);
+
+ $this->assertEquals([
+ 'time_network' => $network,
+ 'time_server' => $server,
+ 'time_transfer' => $transfer,
+ 'time_dom_processing' => $domProcessing,
+ 'time_dom_completion' => $domCompletion,
+ 'time_on_load' => $onload
+ ], $result);
+ }
+} \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/System/APITest.php b/plugins/PagePerformance/tests/System/APITest.php
new file mode 100644
index 0000000000..13af42ff57
--- /dev/null
+++ b/plugins/PagePerformance/tests/System/APITest.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\PagePerformance\tests\System;
+
+use Piwik\Plugins\PagePerformance\tests\Fixtures\VisitsWithPagePerformanceMetrics;
+use Piwik\Tests\Framework\TestCase\SystemTestCase;
+
+/**
+ * @group PagePerformance
+ * @group APITest
+ * @group Plugins
+ */
+class APITest extends SystemTestCase
+{
+ /**
+ * @var VisitsWithPagePerformanceMetrics
+ */
+ public static $fixture = null; // initialized below class definition
+
+ /**
+ * @dataProvider getApiForTesting
+ */
+ public function testApi($api, $params)
+ {
+ $this->runApiTests($api, $params);
+ }
+
+ public function getApiForTesting()
+ {
+ $api = array(
+ 'PagePerformance.get',
+ 'Actions.getPageUrls',
+ 'Actions.getPageTitles',
+ );
+
+ $apiToTest = array();
+ $apiToTest[] = array($api,
+ array(
+ 'idSite' => 1,
+ 'date' => self::$fixture->dateTime,
+ 'periods' => array('day', 'month'),
+ 'testSuffix' => ''
+ )
+ );
+
+ return $apiToTest;
+ }
+
+ public static function getOutputPrefix()
+ {
+ return '';
+ }
+
+ public static function getPathToTestDirectory()
+ {
+ return dirname(__FILE__);
+ }
+
+}
+
+APITest::$fixture = new VisitsWithPagePerformanceMetrics(); \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/System/expected/test___Actions.getPageTitles_day.xml b/plugins/PagePerformance/tests/System/expected/test___Actions.getPageTitles_day.xml
new file mode 100644
index 0000000000..1d36a79dd6
--- /dev/null
+++ b/plugins/PagePerformance/tests/System/expected/test___Actions.getPageTitles_day.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label> Contact</label>
+ <nb_visits>1</nb_visits>
+ <nb_uniq_visitors>1</nb_uniq_visitors>
+ <nb_hits>2</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>2</nb_hits_with_time_network>
+ <min_time_network>0.0000</min_time_network>
+ <max_time_network>0.0360</max_time_network>
+ <nb_hits_with_time_server>2</nb_hits_with_time_server>
+ <min_time_server>0.0770</min_time_server>
+ <max_time_server>0.0990</max_time_server>
+ <nb_hits_with_time_transfer>2</nb_hits_with_time_transfer>
+ <min_time_transfer>0.2980</min_time_transfer>
+ <max_time_transfer>0.4120</max_time_transfer>
+ <nb_hits_with_time_dom_processing>2</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>0.9990</min_time_dom_processing>
+ <max_time_dom_processing>1.0550</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>2</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>0.3330</min_time_dom_completion>
+ <max_time_dom_completion>0.4120</max_time_dom_completion>
+ <nb_hits_with_time_on_load>2</nb_hits_with_time_on_load>
+ <min_time_on_load>0.0770</min_time_on_load>
+ <max_time_on_load>0.2320</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <exit_nb_uniq_visitors>1</exit_nb_uniq_visitors>
+ <exit_nb_visits>1</exit_nb_visits>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.018</avg_time_network>
+ <avg_time_server>0.088</avg_time_server>
+ <avg_time_transfer>0.355</avg_time_transfer>
+ <avg_time_dom_processing>1.027</avg_time_dom_processing>
+ <avg_time_dom_completion>0.373</avg_time_dom_completion>
+ <avg_time_on_load>0.155</avg_time_on_load>
+ <avg_page_load_time>2.016</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>100%</exit_rate>
+ <segment>pageTitle==Contact</segment>
+ </row>
+ <row>
+ <label> Page Title 1</label>
+ <nb_visits>1</nb_visits>
+ <nb_uniq_visitors>1</nb_uniq_visitors>
+ <nb_hits>1</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>1</nb_hits_with_time_network>
+ <min_time_network>0.0120</min_time_network>
+ <max_time_network>0.0120</max_time_network>
+ <nb_hits_with_time_server>1</nb_hits_with_time_server>
+ <min_time_server>0.1500</min_time_server>
+ <max_time_server>0.1500</max_time_server>
+ <nb_hits_with_time_transfer>1</nb_hits_with_time_transfer>
+ <min_time_transfer>0.3330</min_time_transfer>
+ <max_time_transfer>0.3330</max_time_transfer>
+ <nb_hits_with_time_dom_processing>1</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>1.1010</min_time_dom_processing>
+ <max_time_dom_processing>1.1010</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>1</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>0.3690</min_time_dom_completion>
+ <max_time_dom_completion>0.3690</max_time_dom_completion>
+ <nb_hits_with_time_on_load>1</nb_hits_with_time_on_load>
+ <min_time_on_load>0.1500</min_time_on_load>
+ <max_time_on_load>0.1500</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <entry_nb_uniq_visitors>1</entry_nb_uniq_visitors>
+ <entry_nb_visits>1</entry_nb_visits>
+ <entry_nb_actions>3</entry_nb_actions>
+ <entry_sum_visit_length>1</entry_sum_visit_length>
+ <entry_bounce_count>0</entry_bounce_count>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.012</avg_time_network>
+ <avg_time_server>0.15</avg_time_server>
+ <avg_time_transfer>0.333</avg_time_transfer>
+ <avg_time_dom_processing>1.101</avg_time_dom_processing>
+ <avg_time_dom_completion>0.369</avg_time_dom_completion>
+ <avg_time_on_load>0.15</avg_time_on_load>
+ <avg_page_load_time>2.115</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>0%</exit_rate>
+ <segment>pageTitle==Page%2BTitle%2B1</segment>
+ </row>
+</result> \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/System/expected/test___Actions.getPageTitles_month.xml b/plugins/PagePerformance/tests/System/expected/test___Actions.getPageTitles_month.xml
new file mode 100644
index 0000000000..8e486ea3c0
--- /dev/null
+++ b/plugins/PagePerformance/tests/System/expected/test___Actions.getPageTitles_month.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label> Contact</label>
+ <nb_visits>5</nb_visits>
+ <nb_hits>10</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>10</nb_hits_with_time_network>
+ <min_time_network>0.094</min_time_network>
+ <max_time_network>0.276</max_time_network>
+ <nb_hits_with_time_server>10</nb_hits_with_time_server>
+ <min_time_server>0.496</min_time_server>
+ <max_time_server>0.801</max_time_server>
+ <nb_hits_with_time_transfer>10</nb_hits_with_time_transfer>
+ <min_time_transfer>1.268</min_time_transfer>
+ <max_time_transfer>1.683</max_time_transfer>
+ <nb_hits_with_time_dom_processing>10</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>4.989</min_time_dom_processing>
+ <max_time_dom_processing>5.678</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>10</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>1.464</min_time_dom_completion>
+ <max_time_dom_completion>1.683</max_time_dom_completion>
+ <nb_hits_with_time_on_load>10</nb_hits_with_time_on_load>
+ <min_time_on_load>0.498</min_time_on_load>
+ <max_time_on_load>1.126</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <exit_nb_visits>5</exit_nb_visits>
+ <sum_daily_nb_uniq_visitors>5</sum_daily_nb_uniq_visitors>
+ <sum_daily_exit_nb_uniq_visitors>5</sum_daily_exit_nb_uniq_visitors>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.037</avg_time_network>
+ <avg_time_server>0.13</avg_time_server>
+ <avg_time_transfer>0.295</avg_time_transfer>
+ <avg_time_dom_processing>1.067</avg_time_dom_processing>
+ <avg_time_dom_completion>0.315</avg_time_dom_completion>
+ <avg_time_on_load>0.162</avg_time_on_load>
+ <avg_page_load_time>2.006</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>100%</exit_rate>
+ <segment>pageTitle==Contact</segment>
+ </row>
+ <row>
+ <label> Page Title 1</label>
+ <nb_visits>5</nb_visits>
+ <nb_hits>5</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>5</nb_hits_with_time_network>
+ <min_time_network>0.132</min_time_network>
+ <max_time_network>0.132</max_time_network>
+ <nb_hits_with_time_server>5</nb_hits_with_time_server>
+ <min_time_server>1.162</min_time_server>
+ <max_time_server>1.162</max_time_server>
+ <nb_hits_with_time_transfer>5</nb_hits_with_time_transfer>
+ <min_time_transfer>1.512</min_time_transfer>
+ <max_time_transfer>1.512</max_time_transfer>
+ <nb_hits_with_time_dom_processing>5</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>5.549</min_time_dom_processing>
+ <max_time_dom_processing>5.549</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>5</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>1.975</min_time_dom_completion>
+ <max_time_dom_completion>1.975</max_time_dom_completion>
+ <nb_hits_with_time_on_load>5</nb_hits_with_time_on_load>
+ <min_time_on_load>1.129</min_time_on_load>
+ <max_time_on_load>1.129</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <entry_nb_visits>5</entry_nb_visits>
+ <entry_nb_actions>15</entry_nb_actions>
+ <entry_sum_visit_length>5</entry_sum_visit_length>
+ <entry_bounce_count>0</entry_bounce_count>
+ <sum_daily_nb_uniq_visitors>5</sum_daily_nb_uniq_visitors>
+ <sum_daily_entry_nb_uniq_visitors>5</sum_daily_entry_nb_uniq_visitors>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.026</avg_time_network>
+ <avg_time_server>0.232</avg_time_server>
+ <avg_time_transfer>0.302</avg_time_transfer>
+ <avg_time_dom_processing>1.11</avg_time_dom_processing>
+ <avg_time_dom_completion>0.395</avg_time_dom_completion>
+ <avg_time_on_load>0.226</avg_time_on_load>
+ <avg_page_load_time>2.291</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>0%</exit_rate>
+ <segment>pageTitle==Page%2BTitle%2B1</segment>
+ </row>
+</result> \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/System/expected/test___Actions.getPageUrls_day.xml b/plugins/PagePerformance/tests/System/expected/test___Actions.getPageUrls_day.xml
new file mode 100644
index 0000000000..99b55d31f9
--- /dev/null
+++ b/plugins/PagePerformance/tests/System/expected/test___Actions.getPageUrls_day.xml
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label>/Contact</label>
+ <nb_visits>1</nb_visits>
+ <nb_uniq_visitors>1</nb_uniq_visitors>
+ <nb_hits>1</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>1</nb_hits_with_time_network>
+ <min_time_network>0.0000</min_time_network>
+ <max_time_network>0.0000</max_time_network>
+ <nb_hits_with_time_server>1</nb_hits_with_time_server>
+ <min_time_server>0.0990</min_time_server>
+ <max_time_server>0.0990</max_time_server>
+ <nb_hits_with_time_transfer>1</nb_hits_with_time_transfer>
+ <min_time_transfer>0.2980</min_time_transfer>
+ <max_time_transfer>0.2980</max_time_transfer>
+ <nb_hits_with_time_dom_processing>1</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>0.9990</min_time_dom_processing>
+ <max_time_dom_processing>0.9990</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>1</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>0.4120</min_time_dom_completion>
+ <max_time_dom_completion>0.4120</max_time_dom_completion>
+ <nb_hits_with_time_on_load>1</nb_hits_with_time_on_load>
+ <min_time_on_load>0.2320</min_time_on_load>
+ <max_time_on_load>0.2320</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0</avg_time_network>
+ <avg_time_server>0.099</avg_time_server>
+ <avg_time_transfer>0.298</avg_time_transfer>
+ <avg_time_dom_processing>0.999</avg_time_dom_processing>
+ <avg_time_dom_completion>0.412</avg_time_dom_completion>
+ <avg_time_on_load>0.232</avg_time_on_load>
+ <avg_page_load_time>2.04</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>0%</exit_rate>
+ <url>http://example.org/Contact</url>
+ <segment>pageUrl==http%253A%252F%252Fexample.org%252FContact</segment>
+ </row>
+ <row>
+ <label>category</label>
+ <nb_visits>1</nb_visits>
+ <nb_hits>1</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>1</nb_hits_with_time_network>
+ <min_time_network>0.012</min_time_network>
+ <max_time_network>0.012</max_time_network>
+ <nb_hits_with_time_server>1</nb_hits_with_time_server>
+ <min_time_server>0.15</min_time_server>
+ <max_time_server>0.15</max_time_server>
+ <nb_hits_with_time_transfer>1</nb_hits_with_time_transfer>
+ <min_time_transfer>0.333</min_time_transfer>
+ <max_time_transfer>0.333</max_time_transfer>
+ <nb_hits_with_time_dom_processing>1</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>1.101</min_time_dom_processing>
+ <max_time_dom_processing>1.101</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>1</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>0.369</min_time_dom_completion>
+ <max_time_dom_completion>0.369</max_time_dom_completion>
+ <nb_hits_with_time_on_load>1</nb_hits_with_time_on_load>
+ <min_time_on_load>0.15</min_time_on_load>
+ <max_time_on_load>0.15</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <entry_nb_visits>1</entry_nb_visits>
+ <entry_nb_actions>3</entry_nb_actions>
+ <entry_sum_visit_length>1</entry_sum_visit_length>
+ <entry_bounce_count>0</entry_bounce_count>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.012</avg_time_network>
+ <avg_time_server>0.15</avg_time_server>
+ <avg_time_transfer>0.333</avg_time_transfer>
+ <avg_time_dom_processing>1.101</avg_time_dom_processing>
+ <avg_time_dom_completion>0.369</avg_time_dom_completion>
+ <avg_time_on_load>0.15</avg_time_on_load>
+ <avg_page_load_time>2.115</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>0%</exit_rate>
+ <segment>pageUrl=^http%253A%252F%252Fpiwik.net%252Fcategory</segment>
+ <subtable>
+ <row>
+ <label>/Page1</label>
+ <nb_visits>1</nb_visits>
+ <nb_uniq_visitors>1</nb_uniq_visitors>
+ <nb_hits>1</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>1</nb_hits_with_time_network>
+ <min_time_network>0.0120</min_time_network>
+ <max_time_network>0.0120</max_time_network>
+ <nb_hits_with_time_server>1</nb_hits_with_time_server>
+ <min_time_server>0.1500</min_time_server>
+ <max_time_server>0.1500</max_time_server>
+ <nb_hits_with_time_transfer>1</nb_hits_with_time_transfer>
+ <min_time_transfer>0.3330</min_time_transfer>
+ <max_time_transfer>0.3330</max_time_transfer>
+ <nb_hits_with_time_dom_processing>1</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>1.1010</min_time_dom_processing>
+ <max_time_dom_processing>1.1010</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>1</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>0.3690</min_time_dom_completion>
+ <max_time_dom_completion>0.3690</max_time_dom_completion>
+ <nb_hits_with_time_on_load>1</nb_hits_with_time_on_load>
+ <min_time_on_load>0.1500</min_time_on_load>
+ <max_time_on_load>0.1500</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <entry_nb_uniq_visitors>1</entry_nb_uniq_visitors>
+ <entry_nb_visits>1</entry_nb_visits>
+ <entry_nb_actions>3</entry_nb_actions>
+ <entry_sum_visit_length>1</entry_sum_visit_length>
+ <entry_bounce_count>0</entry_bounce_count>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>0%</exit_rate>
+ <url>http://example.org/category/Page1</url>
+ <segment>pageUrl==http%253A%252F%252Fexample.org%252Fcategory%252FPage1</segment>
+ </row>
+ </subtable>
+ </row>
+ <row>
+ <label>Contact</label>
+ <nb_visits>1</nb_visits>
+ <nb_hits>1</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>1</nb_hits_with_time_network>
+ <min_time_network>0.036</min_time_network>
+ <max_time_network>0.036</max_time_network>
+ <nb_hits_with_time_server>1</nb_hits_with_time_server>
+ <min_time_server>0.077</min_time_server>
+ <max_time_server>0.077</max_time_server>
+ <nb_hits_with_time_transfer>1</nb_hits_with_time_transfer>
+ <min_time_transfer>0.412</min_time_transfer>
+ <max_time_transfer>0.412</max_time_transfer>
+ <nb_hits_with_time_dom_processing>1</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>1.055</min_time_dom_processing>
+ <max_time_dom_processing>1.055</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>1</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>0.333</min_time_dom_completion>
+ <max_time_dom_completion>0.333</max_time_dom_completion>
+ <nb_hits_with_time_on_load>1</nb_hits_with_time_on_load>
+ <min_time_on_load>0.077</min_time_on_load>
+ <max_time_on_load>0.077</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <exit_nb_visits>1</exit_nb_visits>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.036</avg_time_network>
+ <avg_time_server>0.077</avg_time_server>
+ <avg_time_transfer>0.412</avg_time_transfer>
+ <avg_time_dom_processing>1.055</avg_time_dom_processing>
+ <avg_time_dom_completion>0.333</avg_time_dom_completion>
+ <avg_time_on_load>0.077</avg_time_on_load>
+ <avg_page_load_time>1.99</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>100%</exit_rate>
+ <segment>pageUrl=^http%253A%252F%252Fpiwik.net%252FContact</segment>
+ <subtable>
+ <row>
+ <label>/ThankYou</label>
+ <nb_visits>1</nb_visits>
+ <nb_uniq_visitors>1</nb_uniq_visitors>
+ <nb_hits>1</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>1</nb_hits_with_time_network>
+ <min_time_network>0.0360</min_time_network>
+ <max_time_network>0.0360</max_time_network>
+ <nb_hits_with_time_server>1</nb_hits_with_time_server>
+ <min_time_server>0.0770</min_time_server>
+ <max_time_server>0.0770</max_time_server>
+ <nb_hits_with_time_transfer>1</nb_hits_with_time_transfer>
+ <min_time_transfer>0.4120</min_time_transfer>
+ <max_time_transfer>0.4120</max_time_transfer>
+ <nb_hits_with_time_dom_processing>1</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>1.0550</min_time_dom_processing>
+ <max_time_dom_processing>1.0550</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>1</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>0.3330</min_time_dom_completion>
+ <max_time_dom_completion>0.3330</max_time_dom_completion>
+ <nb_hits_with_time_on_load>1</nb_hits_with_time_on_load>
+ <min_time_on_load>0.0770</min_time_on_load>
+ <max_time_on_load>0.0770</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <exit_nb_uniq_visitors>1</exit_nb_uniq_visitors>
+ <exit_nb_visits>1</exit_nb_visits>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>100%</exit_rate>
+ <url>http://example.org/Contact/ThankYou</url>
+ <segment>pageUrl==http%253A%252F%252Fexample.org%252FContact%252FThankYou</segment>
+ </row>
+ </subtable>
+ </row>
+</result> \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/System/expected/test___Actions.getPageUrls_month.xml b/plugins/PagePerformance/tests/System/expected/test___Actions.getPageUrls_month.xml
new file mode 100644
index 0000000000..aa8308340e
--- /dev/null
+++ b/plugins/PagePerformance/tests/System/expected/test___Actions.getPageUrls_month.xml
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label>/Contact</label>
+ <nb_visits>5</nb_visits>
+ <nb_hits>5</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>5</nb_hits_with_time_network>
+ <min_time_network>0.205</min_time_network>
+ <max_time_network>0.205</max_time_network>
+ <nb_hits_with_time_server>5</nb_hits_with_time_server>
+ <min_time_server>0.54</min_time_server>
+ <max_time_server>0.54</max_time_server>
+ <nb_hits_with_time_transfer>5</nb_hits_with_time_transfer>
+ <min_time_transfer>1.278</min_time_transfer>
+ <max_time_transfer>1.278</max_time_transfer>
+ <nb_hits_with_time_dom_processing>5</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>5.311</min_time_dom_processing>
+ <max_time_dom_processing>5.311</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>5</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>1.587</min_time_dom_completion>
+ <max_time_dom_completion>1.587</max_time_dom_completion>
+ <nb_hits_with_time_on_load>5</nb_hits_with_time_on_load>
+ <min_time_on_load>0.732</min_time_on_load>
+ <max_time_on_load>0.732</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <sum_daily_nb_uniq_visitors>5</sum_daily_nb_uniq_visitors>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.041</avg_time_network>
+ <avg_time_server>0.108</avg_time_server>
+ <avg_time_transfer>0.256</avg_time_transfer>
+ <avg_time_dom_processing>1.062</avg_time_dom_processing>
+ <avg_time_dom_completion>0.317</avg_time_dom_completion>
+ <avg_time_on_load>0.146</avg_time_on_load>
+ <avg_page_load_time>1.93</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>0%</exit_rate>
+ <url>http://example.org/Contact</url>
+ <segment>pageUrl==http%253A%252F%252Fexample.org%252FContact</segment>
+ </row>
+ <row>
+ <label>category</label>
+ <nb_visits>5</nb_visits>
+ <nb_hits>5</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>5</nb_hits_with_time_network>
+ <min_time_network>0.132</min_time_network>
+ <max_time_network>0.132</max_time_network>
+ <nb_hits_with_time_server>5</nb_hits_with_time_server>
+ <min_time_server>1.162</min_time_server>
+ <max_time_server>1.162</max_time_server>
+ <nb_hits_with_time_transfer>5</nb_hits_with_time_transfer>
+ <min_time_transfer>1.512</min_time_transfer>
+ <max_time_transfer>1.512</max_time_transfer>
+ <nb_hits_with_time_dom_processing>5</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>5.549</min_time_dom_processing>
+ <max_time_dom_processing>5.549</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>5</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>1.975</min_time_dom_completion>
+ <max_time_dom_completion>1.975</max_time_dom_completion>
+ <nb_hits_with_time_on_load>5</nb_hits_with_time_on_load>
+ <min_time_on_load>1.129</min_time_on_load>
+ <max_time_on_load>1.129</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <entry_nb_visits>5</entry_nb_visits>
+ <entry_nb_actions>15</entry_nb_actions>
+ <entry_sum_visit_length>5</entry_sum_visit_length>
+ <entry_bounce_count>0</entry_bounce_count>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.026</avg_time_network>
+ <avg_time_server>0.232</avg_time_server>
+ <avg_time_transfer>0.302</avg_time_transfer>
+ <avg_time_dom_processing>1.11</avg_time_dom_processing>
+ <avg_time_dom_completion>0.395</avg_time_dom_completion>
+ <avg_time_on_load>0.226</avg_time_on_load>
+ <avg_page_load_time>2.291</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>0%</exit_rate>
+ <segment>pageUrl=^http%253A%252F%252Fpiwik.net%252Fcategory</segment>
+ <subtable>
+ <row>
+ <label>/Page1</label>
+ <nb_visits>5</nb_visits>
+ <nb_hits>5</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>5</nb_hits_with_time_network>
+ <min_time_network>0.132</min_time_network>
+ <max_time_network>0.132</max_time_network>
+ <nb_hits_with_time_server>5</nb_hits_with_time_server>
+ <min_time_server>1.162</min_time_server>
+ <max_time_server>1.162</max_time_server>
+ <nb_hits_with_time_transfer>5</nb_hits_with_time_transfer>
+ <min_time_transfer>1.512</min_time_transfer>
+ <max_time_transfer>1.512</max_time_transfer>
+ <nb_hits_with_time_dom_processing>5</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>5.549</min_time_dom_processing>
+ <max_time_dom_processing>5.549</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>5</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>1.975</min_time_dom_completion>
+ <max_time_dom_completion>1.975</max_time_dom_completion>
+ <nb_hits_with_time_on_load>5</nb_hits_with_time_on_load>
+ <min_time_on_load>1.129</min_time_on_load>
+ <max_time_on_load>1.129</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <entry_nb_visits>5</entry_nb_visits>
+ <entry_nb_actions>15</entry_nb_actions>
+ <entry_sum_visit_length>5</entry_sum_visit_length>
+ <entry_bounce_count>0</entry_bounce_count>
+ <sum_daily_nb_uniq_visitors>5</sum_daily_nb_uniq_visitors>
+ <sum_daily_entry_nb_uniq_visitors>5</sum_daily_entry_nb_uniq_visitors>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>0%</exit_rate>
+ <url>http://example.org/category/Page1</url>
+ <segment>pageUrl==http%253A%252F%252Fexample.org%252Fcategory%252FPage1</segment>
+ </row>
+ </subtable>
+ </row>
+ <row>
+ <label>Contact</label>
+ <nb_visits>5</nb_visits>
+ <nb_hits>5</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>5</nb_hits_with_time_network>
+ <min_time_network>0.165</min_time_network>
+ <max_time_network>0.165</max_time_network>
+ <nb_hits_with_time_server>5</nb_hits_with_time_server>
+ <min_time_server>0.757</min_time_server>
+ <max_time_server>0.757</max_time_server>
+ <nb_hits_with_time_transfer>5</nb_hits_with_time_transfer>
+ <min_time_transfer>1.673</min_time_transfer>
+ <max_time_transfer>1.673</max_time_transfer>
+ <nb_hits_with_time_dom_processing>5</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>5.356</min_time_dom_processing>
+ <max_time_dom_processing>5.356</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>5</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>1.56</min_time_dom_completion>
+ <max_time_dom_completion>1.56</max_time_dom_completion>
+ <nb_hits_with_time_on_load>5</nb_hits_with_time_on_load>
+ <min_time_on_load>0.892</min_time_on_load>
+ <max_time_on_load>0.892</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <exit_nb_visits>5</exit_nb_visits>
+ <avg_bandwidth>0</avg_bandwidth>
+ <avg_time_network>0.033</avg_time_network>
+ <avg_time_server>0.151</avg_time_server>
+ <avg_time_transfer>0.335</avg_time_transfer>
+ <avg_time_dom_processing>1.071</avg_time_dom_processing>
+ <avg_time_dom_completion>0.312</avg_time_dom_completion>
+ <avg_time_on_load>0.178</avg_time_on_load>
+ <avg_page_load_time>2.08</avg_page_load_time>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>100%</exit_rate>
+ <segment>pageUrl=^http%253A%252F%252Fpiwik.net%252FContact</segment>
+ <subtable>
+ <row>
+ <label>/ThankYou</label>
+ <nb_visits>5</nb_visits>
+ <nb_hits>5</nb_hits>
+ <sum_time_spent>0</sum_time_spent>
+ <nb_hits_with_time_network>5</nb_hits_with_time_network>
+ <min_time_network>0.165</min_time_network>
+ <max_time_network>0.165</max_time_network>
+ <nb_hits_with_time_server>5</nb_hits_with_time_server>
+ <min_time_server>0.757</min_time_server>
+ <max_time_server>0.757</max_time_server>
+ <nb_hits_with_time_transfer>5</nb_hits_with_time_transfer>
+ <min_time_transfer>1.673</min_time_transfer>
+ <max_time_transfer>1.673</max_time_transfer>
+ <nb_hits_with_time_dom_processing>5</nb_hits_with_time_dom_processing>
+ <min_time_dom_processing>5.356</min_time_dom_processing>
+ <max_time_dom_processing>5.356</max_time_dom_processing>
+ <nb_hits_with_time_dom_completion>5</nb_hits_with_time_dom_completion>
+ <min_time_dom_completion>1.56</min_time_dom_completion>
+ <max_time_dom_completion>1.56</max_time_dom_completion>
+ <nb_hits_with_time_on_load>5</nb_hits_with_time_on_load>
+ <min_time_on_load>0.892</min_time_on_load>
+ <max_time_on_load>0.892</max_time_on_load>
+ <sum_bandwidth>0</sum_bandwidth>
+ <nb_hits_with_bandwidth>0</nb_hits_with_bandwidth>
+ <min_bandwidth />
+ <max_bandwidth />
+ <exit_nb_visits>5</exit_nb_visits>
+ <sum_daily_nb_uniq_visitors>5</sum_daily_nb_uniq_visitors>
+ <sum_daily_exit_nb_uniq_visitors>5</sum_daily_exit_nb_uniq_visitors>
+ <avg_time_on_page>0</avg_time_on_page>
+ <bounce_rate>0%</bounce_rate>
+ <exit_rate>100%</exit_rate>
+ <url>http://example.org/Contact/ThankYou</url>
+ <segment>pageUrl==http%253A%252F%252Fexample.org%252FContact%252FThankYou</segment>
+ </row>
+ </subtable>
+ </row>
+</result> \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/System/expected/test___PagePerformance.get_day.xml b/plugins/PagePerformance/tests/System/expected/test___PagePerformance.get_day.xml
new file mode 100644
index 0000000000..703eef6f74
--- /dev/null
+++ b/plugins/PagePerformance/tests/System/expected/test___PagePerformance.get_day.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <avg_time_network>0.02</avg_time_network>
+ <avg_time_server>0.11</avg_time_server>
+ <avg_time_transfer>0.35</avg_time_transfer>
+ <avg_time_dom_processing>1.05</avg_time_dom_processing>
+ <avg_time_dom_completion>0.37</avg_time_dom_completion>
+ <avg_time_on_load>0.15</avg_time_on_load>
+ <avg_page_load_time>2.05</avg_page_load_time>
+</result> \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/System/expected/test___PagePerformance.get_month.xml b/plugins/PagePerformance/tests/System/expected/test___PagePerformance.get_month.xml
new file mode 100644
index 0000000000..8b3657c4c5
--- /dev/null
+++ b/plugins/PagePerformance/tests/System/expected/test___PagePerformance.get_month.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <avg_time_network>0.03</avg_time_network>
+ <avg_time_server>0.16</avg_time_server>
+ <avg_time_transfer>0.3</avg_time_transfer>
+ <avg_time_dom_processing>1.08</avg_time_dom_processing>
+ <avg_time_dom_completion>0.34</avg_time_dom_completion>
+ <avg_time_on_load>0.18</avg_time_on_load>
+ <avg_page_load_time>2.1</avg_page_load_time>
+</result> \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/UI/PagePerformance_spec.js b/plugins/PagePerformance/tests/UI/PagePerformance_spec.js
new file mode 100644
index 0000000000..4e5ef521d7
--- /dev/null
+++ b/plugins/PagePerformance/tests/UI/PagePerformance_spec.js
@@ -0,0 +1,113 @@
+/*!
+ * Matomo - free/libre analytics platform
+ *
+ * Page Performance screenshot tests.
+ *
+ * @link https://matomo.org
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe("PagePerformance", function () {
+
+ this.timeout(0);
+ this.fixture = "Piwik\\Plugins\\PagePerformance\\tests\\Fixtures\\VisitsWithPagePerformanceMetrics";
+
+ const generalParams = 'idSite=1&period=day&date=2010-03-12',
+ urlBase = 'module=CoreHome&action=index&' + generalParams;
+
+ it("should load page performance overview", async function () {
+ await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Visitors&subcategory=General_Overview");
+ pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('load');
+ });
+
+ it("should show new row action in pages reports", async function () {
+ await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=General_Pages");
+
+ // hover first row
+ const row = await page.waitForSelector('.dataTable tbody tr:first-child');
+ await row.hover();
+ await page.waitFor(50);
+
+ pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('rowactions');
+ });
+
+ it("should load page performance overlay", async function () {
+
+ // click page performance icon
+ const icon = await page.waitForSelector('.dataTable tbody tr:first-child a.actionPagePerformance');
+ await icon.click();
+
+ await page.waitForNetworkIdle();
+
+ pageWrap = await page.waitForSelector('.ui-dialog');
+
+ await page.hover('.piwik-graph');
+ await page.waitFor(50);
+
+ expect(await pageWrap.screenshot()).to.matchImage('pageurl_overlay');
+ });
+
+ it("should show new table with performance metrics visualization in selection", async function () {
+ await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=General_Pages");
+
+ // hover visualization selection
+ const icon = await page.jQuery('.activateVisualizationSelection');
+ await icon.click();
+
+ pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('visualizations');
+ });
+
+ it("should load new table with performance metrics visualization", async function () {
+
+ // hover visualization selection
+ const icon = await page.jQuery('.dropdown-content .icon-page-performance');
+ await icon.click();
+
+ await page.waitForNetworkIdle();
+
+ pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('performance_visualization');
+ });
+
+ it("should show rowaction for subtable rows", async function () {
+
+ const subtablerow = await page.jQuery('tr.subDataTable:eq(1)');
+ await subtablerow.click();
+
+ await page.waitForNetworkIdle();
+ await page.waitFor(200);
+
+ // hover first row
+ const row = await page.jQuery('tr.subDataTable:eq(1) + tr');
+ await row.hover();
+
+ pageWrap = await page.$('.pageWrap');
+ expect(await pageWrap.screenshot()).to.matchImage('rowactions_subtable');
+ });
+
+ it("performance overlay should work on page titles report", async function () {
+ await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=Actions_SubmenuPageTitles");
+
+ // hover first row
+ const row = await page.waitForSelector('.dataTable tbody tr:first-child');
+ await row.hover();
+
+ // click page performance icon
+ const icon = await page.waitForSelector('.dataTable tbody tr:first-child a.actionPagePerformance');
+ await icon.click();
+
+ await page.waitForNetworkIdle();
+
+ pageWrap = await page.waitForSelector('.ui-dialog');
+
+ await page.hover('.piwik-graph');
+ await page.waitFor(50);
+
+ expect(await pageWrap.screenshot()).to.matchImage('pagetitle_overlay');
+ });
+
+
+}); \ No newline at end of file
diff --git a/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_load.png b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_load.png
new file mode 100644
index 0000000000..bb5f68553b
--- /dev/null
+++ b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_load.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c7ba0c0ca454c7bb4c00a2ae6e966e9fb9637fac38294128412b0942f6b59323
+size 121408
diff --git a/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_pagetitle_overlay.png b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_pagetitle_overlay.png
new file mode 100644
index 0000000000..5188f0121b
--- /dev/null
+++ b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_pagetitle_overlay.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef1dc8a36e65969bc20985ed2c30d14f6336cabe3d354fb80b51bc1c09cbe918
+size 144756
diff --git a/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_pageurl_overlay.png b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_pageurl_overlay.png
new file mode 100644
index 0000000000..f4989d4fb0
--- /dev/null
+++ b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_pageurl_overlay.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:36d601ddebcf92df4e366bf063f8f98220a133f9b3bf70f0da6a43420f3bd3e2
+size 144400
diff --git a/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_performance_visualization.png b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_performance_visualization.png
new file mode 100644
index 0000000000..b589d99e5e
--- /dev/null
+++ b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_performance_visualization.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:877862d041ba055d937047623743ffc37470241c0264de3f38d623f408ebf106
+size 31577
diff --git a/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_rowactions.png b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_rowactions.png
new file mode 100644
index 0000000000..d29e0932fb
--- /dev/null
+++ b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_rowactions.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a798e897f7009355c7466f570c8dc30bf33a0aac2f506c6e392f4bccb5b56b86
+size 32214
diff --git a/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_rowactions_subtable.png b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_rowactions_subtable.png
new file mode 100644
index 0000000000..1589b404a7
--- /dev/null
+++ b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_rowactions_subtable.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:789dda219d0921b01c6cb27e86b7adec6319382e707285a12c6009bcad3c931f
+size 38917
diff --git a/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_visualizations.png b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_visualizations.png
new file mode 100644
index 0000000000..34627bb38e
--- /dev/null
+++ b/plugins/PagePerformance/tests/UI/expected-screenshots/PagePerformance_visualizations.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cfcbe8cf8634bc9e41920c5148b7b376b28d97cc2c9d14589b57ba29b4d013b1
+size 41428