From c17139710a43b55b5f48377e6281db6ae602957c Mon Sep 17 00:00:00 2001 From: mattab <matthieu.aubry@gmail.com> Date: Mon, 10 Jun 2013 21:00:42 +1200 Subject: [PATCH] refactoring / improvements of Archiveprocessing (in progress) such as removing duplicate code and a lot of refactoring, the code is now much more readable! --- core/Archive.php | 23 +- core/ArchiveProcessing.php | 12 +- core/ArchiveProcessing/Day.php | 908 ++++++------------ core/DataAccess/LogAggregator.php | 40 +- core/DataTable.php | 44 +- core/DataTable/Row.php | 2 +- core/PluginsArchiver.php | 1 - core/Segment.php | 2 - plugins/Actions/Archiver.php | 12 +- plugins/CustomVariables/Archiver.php | 106 +- plugins/DBStats/API.php | 4 +- plugins/DevicesDetection/Archiver.php | 84 +- plugins/ExampleUI/API.php | 15 +- plugins/Goals/Archiver.php | 459 +++++---- plugins/Provider/Archiver.php | 13 +- plugins/Referers/API.php | 20 +- plugins/Referers/Archiver.php | 243 ++--- plugins/SEO/API.php | 4 +- plugins/Transitions/Transitions.php | 62 +- plugins/UserCountry/API.php | 8 +- plugins/UserCountry/Archiver.php | 135 ++- plugins/UserSettings/API.php | 1 - plugins/UserSettings/Archiver.php | 95 +- plugins/VisitTime/Archiver.php | 50 +- plugins/VisitorInterest/Archiver.php | 56 +- ...mVariables_SegmentMatchVisitorTypeTest.php | 3 + 26 files changed, 956 insertions(+), 1446 deletions(-) diff --git a/core/Archive.php b/core/Archive.php index a242b456da..f5856d4a99 100644 --- a/core/Archive.php +++ b/core/Archive.php @@ -678,9 +678,10 @@ class Piwik_Archive Piwik::log("Archive $archiveDesc skipped, archive is after today."); continue; } - + // prepare the ArchiveProcessing instance - $processing = $this->getArchiveProcessingInstance($period); + $processing = Piwik_ArchiveProcessing::factory($period->getLabel()); + $processing->setSite($site); $processing->setPeriod($period); $processing->setSegment($this->params->getSegment()); @@ -754,23 +755,7 @@ class Piwik_Archive { return Piwik_ArchiveProcessing::getDoneStringFlagFor($this->params->getSegment(), $this->getPeriodLabel(), $plugin); } - - /** - * Returns an ArchiveProcessing instance that should be used for a specific - * period. - * - * @param Piwik_Period $period - * @return Piwik_ArchiveProcessing - */ - private function getArchiveProcessingInstance($period) - { - $label = $period->getLabel(); - if (!isset($this->processingCache[$label])) { - $this->processingCache[$label] = Piwik_ArchiveProcessing::factory($label); - } - return $this->processingCache[$label]; - } - + private function getPeriodLabel() { $periods = $this->params->getPeriods(); diff --git a/core/ArchiveProcessing.php b/core/ArchiveProcessing.php index 738bc13fa0..5785458e18 100644 --- a/core/ArchiveProcessing.php +++ b/core/ArchiveProcessing.php @@ -532,6 +532,13 @@ abstract class Piwik_ArchiveProcessing return $this->insertRecord($name, $value); } + public function insertNumericRecords($numericRecords) + { + foreach ($numericRecords as $name => $value) { + $this->insertNumericRecord($name, $value); + } + } + /** * @param string $name * @param string|array $values @@ -547,6 +554,7 @@ abstract class Piwik_ArchiveProcessing // but for the child table of 'Google' which has the ID = 9 the name would be 'referer_search_engine_9' $newName = $name; if ($id != 0) { + //FIXMEA: refactor $newName = $name . '_' . $id; } @@ -630,7 +638,7 @@ abstract class Piwik_ArchiveProcessing protected function insertRecord($name, $value) { // We choose not to record records with a value of 0 - if ($value === 0) { + if ($value == 0) { return; } $tableName = $this->getTableNameToInsert($value); @@ -831,7 +839,7 @@ abstract class Piwik_ArchiveProcessing protected function isProcessingEnabled() { return $this->shouldProcessReportsAllPlugins($this->getSegment(), $this->getPeriod()->getLabel()) - || ($this->isVisitsSummaryRequested()); + || $this->isVisitsSummaryRequested(); } /** diff --git a/core/ArchiveProcessing/Day.php b/core/ArchiveProcessing/Day.php index 41a3e3c556..b662ca54d0 100644 --- a/core/ArchiveProcessing/Day.php +++ b/core/ArchiveProcessing/Day.php @@ -21,269 +21,36 @@ */ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing { - public function getDb() - { - return Zend_Registry::get('db'); - } - - /** - * Main method to process logs for a day. The only logic done here is computing the number of visits, actions, etc. - * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Day.compute'. - * See some of the plugins for an example eg. 'Provider' - */ - protected function compute() - { - if (!$this->isThereSomeVisits()) { - return; - } - Piwik_PostEvent('ArchiveProcessing_Day.compute', $this); - } - - /** - * Returns true if there are logs for the current archive. - * - * If the current archive is for a specific plugin (for example, Referers), - * (for example when a Segment is defined and the Keywords report is requested) - * Then the function will create the Archive for the Core metrics 'VisitsSummary' which will in turn process the number of visits - * - * If there is no specified segment, the SQL query will always run. - * - * @return bool|null - */ - public function isThereSomeVisits() - { - if (!is_null($this->isThereSomeVisits)) { - if ($this->isThereSomeVisits && is_null($this->nb_visits)) { - debug_print_backtrace(); - exit; - } - return $this->isThereSomeVisits; - } - - if (!$this->isProcessingEnabled()) { - return $this->redirectRequestToVisitsSummary(); - } - - $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors, - count(*) as nb_visits, - sum(log_visit.visit_total_actions) as nb_actions, - max(log_visit.visit_total_actions) as max_actions, - sum(log_visit.visit_total_time) as sum_visit_length, - sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as bounce_count, - sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted - "; - $from = "log_visit"; - $where = "log_visit.visit_last_action_time >= ? - AND log_visit.visit_last_action_time <= ? - AND log_visit.idsite = ? - "; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->getSite()->getId()); - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind); - - $bind = $query['bind']; - $sql = $query['sql']; - - $data = $this->getDb()->fetchRow($sql, $bind); - - // no visits found - if (!is_array($data) || $data['nb_visits'] == 0) { - return $this->isThereSomeVisits = false; - } - - // visits found: set attribtues - foreach ($data as $name => $value) { - $this->insertNumericRecord($name, $value); - } - - $this->setNumberOfVisits($data['nb_visits']); - $this->setNumberOfVisitsConverted($data['nb_visits_converted']); - - return $this->isThereSomeVisits = true; - } - - /** - * If a segment is specified but a plugin other than 'VisitsSummary' is being requested, - * we create an archive for processing VisitsSummary Core Metrics, which will in turn - * execute the query above (in isThereSomeVisits) - * - * @return bool|null - */ - private function redirectRequestToVisitsSummary() - { - $archive = $this->makeNewArchive(); - $nbVisits = $archive->getNumeric('nb_visits'); - $this->isThereSomeVisits = $nbVisits > 0; - - if ($this->isThereSomeVisits) { - $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); - $this->setNumberOfVisits($nbVisits); - $this->setNumberOfVisitsConverted($nbVisitsConverted); - } - - return $this->isThereSomeVisits; - } - - /** - * Converts a database SELECT result into a whole DataTable with two columns and as many - * rows as elements in $row. - * - * The key of each element in $row is used as the value of the first column, and the - * value of each element is used as the second column. - * - * NOTE: $selectAsPrefix can be used to make sure only SOME of the data in $row is used. - * - * @param array $row The database row to convert. - * @param mixed $labelCount The label to use for the second column of the DataTable result. - * @param string $selectAsPrefix A string that identifies which elements of $row to use - * in the result. Every key of $row that starts with this - * value is used. - * @return Piwik_DataTable - */ - public function getSimpleDataTableFromRow($row, $labelCount, $selectAsPrefix = '') - { - // the labels in $row can have prefixes that need to be removed before creating a table - $cleanRow = array(); + const FIELDS_SEPARATOR = ", \n\t\t\t"; + const LOG_CONVERSION_TABLE = "log_conversion"; + const REVENUE_SUBTOTAL_FIELD = 'revenue_subtotal'; + const REVENUE_TAX_FIELD = 'revenue_tax'; + const REVENUE_SHIPPING_FIELD = 'revenue_shipping'; + const REVENUE_DISCOUNT_FIELD = 'revenue_discount'; + const TOTAL_REVENUE_FIELD = 'revenue'; + const ITEMS_COUNT_FIELD = "items"; - foreach ($row as $label => $count) { - if (empty($selectAsPrefix) || strpos($label, $selectAsPrefix) === 0) { - $cleanLabel = substr($label, strlen($selectAsPrefix)); + const IDGOAL_FIELD = 'idgoal'; - $cleanRow[$cleanLabel] = array($labelCount => $count); - } - } + const CONVERSION_DATETIME_FIELD = "server_time"; + const ACTION_DATETIME_FIELD = "server_time"; - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($cleanRow); - return $table; - } + const VISIT_DATETIME_FIELD = 'visit_last_action_time'; + const LOG_ACTIONS_TABLE = 'log_link_visit_action'; - /** - * Helper function that returns a DataTable containing the $select fields / value pairs. - * IMPORTANT: The $select must return only one row!! - * - * Example $select = "count(distinct( config_os )) as countDistinctOs, - * sum( config_flash ) / count(distinct(idvisit)) as percentFlash " - * $labelCount = "test_column_name" - * will return a dataTable that looks like - * label test_column_name - * CountDistinctOs 9 - * PercentFlash 0.5676 - * - * - * @param string $select - * @param string $labelCount - * @return Piwik_DataTable - */ - public function getSimpleDataTableFromSelect($select, $labelCount) - { - $data = $this->queryVisitsSimple($select); - return $this->getSimpleDataTableFromRow($data, $labelCount); - } - - /** - * Performs a simple query on the log_visit table within the time range this archive - * represents. - * - * @param string $select The SELECT clause. - * @param string|bool $orderBy The ORDER BY clause (without the 'ORDER BY' part). Set to - * false to specify no ORDER BY. - * @param array|bool $groupByCols An array of column names to group by. Set to false to - * specify no GROUP BY. - * @param bool $oneResultRow Whether only one row is expected or not. If set to true, - * this function returns one row, if false, an array of rows. - * @return array - */ - public function queryVisitsSimple($select, $orderBy = false, $groupByCols = false, $oneResultRow = true) - { - $from = "log_visit"; - $where = "log_visit.visit_last_action_time >= ? - AND log_visit.visit_last_action_time <= ? - AND log_visit.idsite = ?"; - - $groupBy = false; - if ($groupByCols and !empty($groupByCols)) { - $groupBy = implode(',', $groupByCols); - } - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->getSite()->getId()); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + const LOG_VISIT_TABLE = 'log_visit'; - if ($oneResultRow) { - return $this->getDb()->fetchRow($query['sql'], $query['bind']); - } else { - return $this->getDb()->fetchAll($query['sql'], $query['bind']); - } - } - - /** - * Returns the actions by the given dimension - * - * - The basic use case is to use $label and optionally $where. - * - If you want to apply a limit and group the others, use $orderBy to sort the way you - * want the limit to be applied and pass a pre-configured instance of Piwik_RankingQuery. - * The ranking query instance has to have a limit and at least one label column. - * See Piwik_RankingQuery::setLimit() and Piwik_RankingQuery::addLabelColumn(). - * If $rankingQuery is set, the return value is the array returned by - * Piwik_RankingQuery::execute(). - * - By default, the method only queries log_link_visit_action. If you need data from - * log_action (e.g. to partition the result from the ranking query into the different - * action types), use $joinLogActionOnColumn and $addSelect to join log_action and select - * the column you need from log_action. - * - * - * @param array|string $label the dimensions(s) you're interested in - * @param string $where where clause - * @param bool|array $metrics Set this if you want to limit the columns that are returned. - * The possible values in the array are Piwik_Archive::INDEX_*. - * @param bool|string $orderBy order by clause - * @param Piwik_RankingQuery $rankingQuery pre-configured ranking query instance - * @param bool|string $joinLogActionOnColumn column from log_link_visit_action that - * log_action should be joined on. - * can be an array to join multiple times. - * @param bool|string $addSelect additional select clause - * @return mixed - */ - public function queryActionsByDimension($label, $where = '', $metrics = false, $orderBy = false, - $rankingQuery = null, $joinLogActionOnColumn = false, $addSelect = false) + public function queryActionsByDimension($dimensions, $where = '', $additionalSelects = array(), $metrics = false, $rankingQuery = null, $joinLogActionOnColumn = false) { - if (is_array($label)) { - $label2 = $label; - foreach ($label2 as &$field) { - $field = 'log_link_visit_action.' . $field; - } - $groupBy = implode(", ", $label2); - foreach ($label2 as $id => &$field) { - $field = "$field AS " . $label[$id]; - } - $select = implode(", ", $label2); + $tableName = self::LOG_ACTIONS_TABLE; + $availableMetrics = $this->getActionsMetricFields(); - // IF we query Custom Variables scope "page" either: Product SKU, Product Name, - // then we also query the "Product page view" price which was possibly recorded. - if (in_array(reset($label), array('custom_var_k3', 'custom_var_k4', 'custom_var_k5'))) { - $select .= ", " . self::getSqlRevenue("AVG(log_link_visit_action.custom_var_v2)") . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED . "`"; - } - } else { - $select = $label . " AS label "; - $groupBy = 'label'; - } - - if (!empty($where)) { - $where = sprintf($where, "log_link_visit_action", "log_link_visit_action"); - $where = ' AND ' . $where; - } - - $pre = ", \n\t\t\t"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) - $select .= $pre . "count(distinct log_link_visit_action.idvisit) as `" . Piwik_Archive::INDEX_NB_VISITS . "`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) - $select .= $pre . "count(distinct log_link_visit_action.idvisitor) as `" . Piwik_Archive::INDEX_NB_UNIQ_VISITORS . "`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) - $select .= $pre . "count(*) as `" . Piwik_Archive::INDEX_NB_ACTIONS . "`"; - - $from = "log_link_visit_action"; + $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); + $from = array($tableName); + $where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where); + $groupBy = $this->getGroupByStatement($dimensions, $tableName); + $orderBy = false; if ($joinLogActionOnColumn !== false) { $multiJoin = is_array($joinLogActionOnColumn); @@ -291,12 +58,10 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing $joinLogActionOnColumn = array($joinLogActionOnColumn); } - $from = array($from); - foreach ($joinLogActionOnColumn as $i => $joinColumn) { $tableAlias = 'log_action' . ($multiJoin ? $i + 1 : ''); if (strpos($joinColumn, ' ') === false) { - $joinOn = $tableAlias . '.idaction = log_link_visit_action.' . $joinColumn; + $joinOn = $tableAlias . '.idaction = ' . $tableName . '.' . $joinColumn; } else { // more complex join column like IF(...) $joinOn = $tableAlias . '.idaction = ' . $joinColumn; @@ -309,32 +74,16 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing } } - if ($addSelect !== false) { - $select .= ', ' . $addSelect; + if ($rankingQuery) { + $orderBy = '`' . Piwik_Archive::INDEX_NB_ACTIONS . '` DESC'; } - $where = "log_link_visit_action.server_time >= ? - AND log_link_visit_action.server_time <= ? - AND log_link_visit_action.idsite = ? - $where"; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->getSite()->getId()); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + $query = $this->query($select, $from, $where, $groupBy, $orderBy); if ($rankingQuery !== null) { - $sumColumns = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_NB_VISITS, - Piwik_Archive::INDEX_NB_ACTIONS - ); + $sumColumns = array_keys($availableMetrics); if ($metrics) { - foreach ($sumColumns as $i => $column) { - if (!in_array($column, $metrics)) { - unset($sumColumns[$i]); - } - } - $sumColumns = array_values($sumColumns); + $sumColumns = array_intersect($sumColumns, $metrics); } $rankingQuery->addColumn($sumColumns, 'sum'); return $rankingQuery->execute($query['sql'], $query['bind']); @@ -343,163 +92,77 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing return $this->getDb()->query($query['sql'], $query['bind']); } - /** - * Query visits by dimension - * - * @param array|string $label Can be a string, eg. "referer_name", will be aliased as 'label' in the returned rows - * Can also be an array of strings, when the dimension spans multiple fields, - * eg. array("referer_name", "referer_keyword") - * @param string $where Additional condition for WHERE clause - * @param bool|array $metrics Set this if you want to limit the columns that are returned. - * The possible values in the array are Piwik_Archive::INDEX_*. - * @param bool|string $orderBy ORDER BY clause. This is needed in combination with $rankingQuery. - * @param Piwik_RankingQuery $rankingQuery - * A pre-configured ranking query instance that is used to limit the result. - * If set, the return value is the array returned by Piwik_RankingQuery::execute(). - * @param bool|string $addSelect Additional SELECT clause - * @param bool $addSelectGeneratesLabelColumn - * Set to true if the $label column is generated in $addSelect. - * - * @return mixed - */ - public function queryVisitsByDimension($label, $where = '', $metrics = false, $orderBy = false, - $rankingQuery = null, $addSelect = false, $addSelectGeneratesLabelColumn = false) + protected function getActionsMetricFields() { - if (is_array($label)) { - $groupBy = "log_visit." . implode(", log_visit.", $label); - foreach ($label as &$field) { - $field = 'log_visit.' . $field . ' AS ' . $field; - } - $select = implode(", ", $label); - } else if ($addSelectGeneratesLabelColumn) { - $select = $addSelect; - $groupBy = $label; - } else { - $select = $label . " AS label "; - $groupBy = 'label'; - } - - if (!empty($where)) { - $where = sprintf($where, "log_visit", "log_visit"); - $where = ' AND ' . $where; - } - - $pre = ", \n\t\t\t"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) - $select .= $pre . "count(distinct log_visit.idvisitor) as `" . Piwik_Archive::INDEX_NB_UNIQ_VISITORS . "`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) - $select .= $pre . "count(*) as `" . Piwik_Archive::INDEX_NB_VISITS . "`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) - $select .= $pre . "sum(log_visit.visit_total_actions) as `" . Piwik_Archive::INDEX_NB_ACTIONS . "`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) - $select .= $pre . "max(log_visit.visit_total_actions) as `" . Piwik_Archive::INDEX_MAX_ACTIONS . "`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_SUM_VISIT_LENGTH, $metrics)) - $select .= $pre . "sum(log_visit.visit_total_time) as `" . Piwik_Archive::INDEX_SUM_VISIT_LENGTH . "`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_BOUNCE_COUNT, $metrics)) - $select .= $pre . "sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `" . Piwik_Archive::INDEX_BOUNCE_COUNT . "`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS_CONVERTED, $metrics)) - $select .= $pre . "sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as `" . Piwik_Archive::INDEX_NB_VISITS_CONVERTED . "`"; - - if ($addSelect && !$addSelectGeneratesLabelColumn) { - $select .= ', ' . $addSelect; - } - - $from = "log_visit"; - - $where = "log_visit.visit_last_action_time >= ? - AND log_visit.visit_last_action_time <= ? - AND log_visit.idsite = ? - $where"; + return $availableMetrics = array( + Piwik_Archive::INDEX_NB_VISITS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisit)", + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_ACTIONS_TABLE . ".idvisitor)", + Piwik_Archive::INDEX_NB_ACTIONS => "count(*)", + ); + } + protected function query($select, $from, $where, $groupBy, $orderBy) + { $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->getSite()->getId()); $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); - - if ($rankingQuery !== null) { - $sumColumns = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS, Piwik_Archive::INDEX_NB_VISITS, - Piwik_Archive::INDEX_NB_ACTIONS, Piwik_Archive::INDEX_SUM_VISIT_LENGTH, - Piwik_Archive::INDEX_BOUNCE_COUNT, Piwik_Archive::INDEX_NB_VISITS_CONVERTED - ); - if ($metrics) { - foreach ($sumColumns as $i => $column) { - if (!in_array($column, $metrics)) { - unset($sumColumns[$i]); - } - } - $sumColumns = array_values($sumColumns); - } - $rankingQuery->addColumn($sumColumns, 'sum'); - if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) { - $rankingQuery->addColumn(Piwik_Archive::INDEX_MAX_ACTIONS, 'max'); - } - return $rankingQuery->execute($query['sql'], $query['bind']); - } - - return $this->getDb()->query($query['sql'], $query['bind']); + return $query; } /** * @see queryVisitsByDimension() Similar to this function, - * but queries metrics for the requested dimensions, + * but queries metrics for the requested dimensionRecord, * for each Goal conversion * - * @param string|array $label + * @param string|array $dimensions * @param string $where - * @param array $aggregateLabels + * @param array $additionalSelects * @return PDOStatement */ - public function queryConversionsByDimension($label, $where = '', $aggregateLabels = array()) + public function queryConversionsByDimension($dimensions = array(), $where = false, $additionalSelects = array()) { - if (empty($label)) { - $select = ""; - $groupBy = ""; - } elseif (is_array($label)) { - $groupBy = "log_conversion." . implode(", log_conversion.", $label); - foreach ($label as &$field) { - $field = 'log_conversion.' . $field . ' AS ' . $field; - } - $select = implode(", ", $label) . ", "; - } else { - $select = $label . " AS label, "; - $groupBy = 'label'; - } - if (!empty($aggregateLabels)) { - $select .= implode(", ", $aggregateLabels) . ", "; - } - if (!empty($where)) { - $where = sprintf($where, "log_conversion", "log_conversion"); - $where = ' AND ' . $where; - } - - $select .= self::getSqlRevenue('SUM(log_conversion.revenue_subtotal)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL . "`," . - self::getSqlRevenue('SUM(log_conversion.revenue_tax)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX . "`," . - self::getSqlRevenue('SUM(log_conversion.revenue_shipping)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING . "`," . - self::getSqlRevenue('SUM(log_conversion.revenue_discount)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT . "`," . - "SUM(log_conversion.items) as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS . "`, "; - - $groupBy = !empty($groupBy) ? ", $groupBy" : ''; - - $select = "$select - log_conversion.idgoal, - count(*) as `" . Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS . "`, - " . self::getSqlRevenue('SUM(log_conversion.revenue)') . " as `" . Piwik_Archive::INDEX_GOAL_REVENUE . "`, - count(distinct log_conversion.idvisit) as `" . Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED . "`"; + $dimensions = array_merge( array(self::IDGOAL_FIELD), $dimensions ); + $availableMetrics = $this->getConversionsMetricFields(); + $tableName = self::LOG_CONVERSION_TABLE; - $from = "log_conversion"; + $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics); - $where = "log_conversion.server_time >= ? - AND log_conversion.server_time <= ? - AND log_conversion.idsite = ? - $where"; - - $groupBy = "log_conversion.idgoal $groupBy"; + $from = array($tableName); + $where = $this->getWhereStatement($tableName, self::CONVERSION_DATETIME_FIELD, $where); + $groupBy = $this->getGroupByStatement($dimensions, $tableName); + $orderBy = false; + $query = $this->query($select, $from, $where, $groupBy, $orderBy); + return $this->getDb()->query($query['sql'], $query['bind']); + } - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->getSite()->getId()); + protected function getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $requestedMetrics = false) + { + $selects = array_merge( + $this->getSelectDimensions($dimensions, $tableName), + $this->getSelectsMetrics($availableMetrics, $requestedMetrics), + !empty($additionalSelects) ? $additionalSelects : array() + ); + $select = implode(self::FIELDS_SEPARATOR, $selects); + return $select; + } - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy = false, $groupBy); + static public function getConversionsMetricFields() + { + return array( + Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => "count(*)", + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => "count(distinct " . self::LOG_CONVERSION_TABLE . ".idvisit)", + Piwik_Archive::INDEX_GOAL_REVENUE => self::getSqlConversionRevenueSum(self::TOTAL_REVENUE_FIELD), + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => self::getSqlConversionRevenueSum(self::REVENUE_SUBTOTAL_FIELD), + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => self::getSqlConversionRevenueSum(self::REVENUE_TAX_FIELD), + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => self::getSqlConversionRevenueSum(self::REVENUE_SHIPPING_FIELD), + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => self::getSqlConversionRevenueSum(self::REVENUE_DISCOUNT_FIELD), + Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => "SUM(" . self::LOG_CONVERSION_TABLE . "." . self::ITEMS_COUNT_FIELD . ")", + ); + } - return $this->getDb()->query($query['sql'], $query['bind']); + static public function getSqlConversionRevenueSum($field) + { + return self::getSqlRevenue('SUM(' . self::LOG_CONVERSION_TABLE . '.' . $field . ')'); } /** @@ -526,7 +189,9 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing AND idsite = ? AND deleted = 0 GROUP BY ecommerceType, $field - ORDER BY NULL"; + ORDER BY null"; + // Segment not supported yet + // $query = $this->query($select, $from, $where, $groupBy, $orderBy); $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), @@ -550,11 +215,19 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing * @param array $array * @return Piwik_DataTable */ - static public function getDataTableFromArray($array) + static public function getDataTableFromDataArray(Piwik_DataArray $array) { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($array); - return $table; + $dataArray = $array->getDataArray(); + $dataArrayTwoLevels = $array->getDataArrayWithTwoLevels(); + + $subtableByLabel = null; + if (!empty($dataArrayTwoLevels)) { + $subtableByLabel = array(); + foreach ($dataArrayTwoLevels as $label => $subTable) { + $subtableByLabel[$label] = Piwik_DataTable::makeFromIndexedArray($subTable); + } + } + return Piwik_DataTable::makeFromIndexedArray($dataArray, $subtableByLabel); } /** @@ -579,7 +252,7 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing * - sum of the visits' length in sec * - count of bouncing visits (visits with one page view) * - * For example if $label = 'config_os' it will return the statistics for every distinct Operating systems + * For example if $dimension = 'config_os' it will return the statistics for every distinct Operating systems * The returned array will have a row per distinct operating systems, * and a column per stat (nb of visits, max actions, etc) * @@ -588,279 +261,250 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing * Windows XP 12 ... * Mac OS 15 36 ... * - * @param string $label Table log_visit field name to be use to compute common stats - * @return array + * @param string $dimension Table log_visit field name to be use to compute common stats + * @return Piwik_DataArray */ - public function getMetricsForLabel($label) + public function getMetricsForDimension($dimension) { - $query = $this->queryVisitsByDimension($label); - $metrics = array(); + if(!is_array($dimension)) { + $dimension = array($dimension); + } + if(count($dimension) == 1) { + $dimension = array("label" => reset($dimension)); + } + $query = $this->queryVisitsByDimension($dimension); + $metrics = new Piwik_DataArray(); while ($row = $query->fetch()) { - if (!isset($metrics[$row['label']])) { - $metrics[$row['label']] = $this->makeEmptyRow(); - } - $this->sumMetrics($row, $metrics[$row['label']]); + $metrics->sumMetricsVisits($row["label"], $row); } return $metrics; } - /** - * Helper function that returns the multiple serialized DataTable of the given PHP array. - * The DataTable here associates a subtable to every row of the level 0 array. - * This is used for example for search engines. - * Every search engine (level 0) has a subtable containing the keywords. - * - * The $arrayLevel0 must have the format - * Example: array ( - * // Yahoo.com => array( kwd1 => stats, kwd2 => stats ) - * LABEL => array(col1 => X, col2 => Y), - * LABEL2 => array(col1 => X, col2 => Y), - * ) + * Returns true if there are logs for the current archive. * - * The $subArrayLevel1ByKey must have the format - * Example: array( - * // Yahoo.com => array( stats ) - * LABEL => #Piwik_DataTable_ForLABEL, - * LABEL2 => #Piwik_DataTable_ForLABEL2, - * ) + * If the current archive is for a specific plugin (for example, Referers), + * (for example when a Segment is defined and the Keywords report is requested) + * Then the function will create the Archive for the Core metrics 'VisitsSummary' which will in turn process the number of visits * + * If there is no specified segment, the SQL query will always run. * - * @param array $arrayLevel0 - * @param array $subArrayLevel1ByKey Array of Piwik_DataTable - * @return array Array with N elements: the strings of the datatable serialized + * @return bool|null */ - public function getDataTableWithSubtablesFromArraysIndexedByLabel($arrayLevel0, $subArrayLevel1ByKey) + public function isThereSomeVisits() { - $parentTableLevel0 = new Piwik_DataTable(); + if (!is_null($this->isThereSomeVisits)) { + return $this->isThereSomeVisits; + } - $tablesByLabel = array(); - foreach ($arrayLevel0 as $label => $aAllRowsForThisLabel) { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($aAllRowsForThisLabel); - $tablesByLabel[$label] = $table; + if (!$this->isProcessingEnabled()) { + return $this->makeArchiveToCheckForVisits(); + } + + $query = $this->queryVisitsByDimension(); + $data = $query->fetch(); + + // no visits found + if (!is_array($data) || $data[Piwik_Archive::INDEX_NB_VISITS] == 0) { + return $this->isThereSomeVisits = false; + } + $metrics = array(); + foreach($data as $metricId => $value) { + $readableMetric = Piwik_Archive::$mappingFromIdToName[$metricId]; + $metrics[$readableMetric] = $value; } - $parentTableLevel0->addRowsFromArrayWithIndexLabel($subArrayLevel1ByKey, $tablesByLabel); + $this->insertNumericRecords($metrics); - return $parentTableLevel0; + $this->setNumberOfVisits($data[Piwik_Archive::INDEX_NB_VISITS]); + $this->setNumberOfVisitsConverted($data[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]); + return $this->isThereSomeVisits = true; } /** - * Returns an empty row containing default metrics + * Query visits by dimension * - * @return array + * @param array|string $dimensions Can be a string, eg. "referer_name", will be aliased as 'label' in the returned rows + * Can also be an array of strings, when the dimension spans multiple fields, + * eg. array("referer_name", "referer_keyword") + * @param bool|string $where Additional condition for WHERE clause + * @param bool|string $additionalSelects Additional SELECT clause + * @param bool|array $metrics Set this if you want to limit the columns that are returned. + * The possible values in the array are Piwik_Archive::INDEX_*. + * @param bool|\Piwik_RankingQuery $rankingQuery + * A pre-configured ranking query instance that is used to limit the result. + * If set, the return value is the array returned by Piwik_RankingQuery::execute(). + * + * @return mixed */ - public function makeEmptyRow() + public function queryVisitsByDimension(array $dimensions = array(), $where = false, array $additionalSelects = array(), $metrics = false, $rankingQuery = false) { - return array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - Piwik_Archive::INDEX_NB_VISITS => 0, - Piwik_Archive::INDEX_NB_ACTIONS => 0, - Piwik_Archive::INDEX_MAX_ACTIONS => 0, - Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 0, - Piwik_Archive::INDEX_BOUNCE_COUNT => 0, - Piwik_Archive::INDEX_NB_VISITS_CONVERTED => 0, - ); - } + $tableName = self::LOG_VISIT_TABLE; + $availableMetrics = $this->getVisitsMetricFields(); + $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); + $from = array($tableName); + $where = $this->getWhereStatement($tableName, self::VISIT_DATETIME_FIELD, $where); + $groupBy = $this->getGroupByStatement($dimensions, $tableName); + $orderBy = false; - /** - * Returns an empty row tracking only Actions - * - * @return array - */ - public function makeEmptyActionRow() + if ($rankingQuery) { + $orderBy = '`' . Piwik_Archive::INDEX_NB_VISITS . '` DESC'; + } + $query = $this->query($select, $from, $where, $groupBy, $orderBy); + + if ($rankingQuery) { + unset($availableMetrics[Piwik_Archive::INDEX_MAX_ACTIONS]); + $sumColumns = array_keys($availableMetrics); + if ($metrics) { + $sumColumns = array_intersect($sumColumns, $metrics); + } + $rankingQuery->addColumn($sumColumns, 'sum'); + if ($this->isMetricRequested(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) { + $rankingQuery->addColumn(Piwik_Archive::INDEX_MAX_ACTIONS, 'max'); + } + return $rankingQuery->execute($query['sql'], $query['bind']); + } + + return $this->getDb()->query($query['sql'], $query['bind']); + } + + protected function getVisitsMetricFields() { return array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - Piwik_Archive::INDEX_NB_VISITS => 0, - Piwik_Archive::INDEX_NB_ACTIONS => 0, + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)", + Piwik_Archive::INDEX_NB_VISITS => "count(*)", + Piwik_Archive::INDEX_NB_ACTIONS => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)", + Piwik_Archive::INDEX_MAX_ACTIONS => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)", + Piwik_Archive::INDEX_SUM_VISIT_LENGTH => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)", + Piwik_Archive::INDEX_BOUNCE_COUNT => "sum(case " . self::LOG_VISIT_TABLE . ".visit_total_actions when 1 then 1 when 0 then 1 else 0 end)", + Piwik_Archive::INDEX_NB_VISITS_CONVERTED => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)", ); } - /** - * @param $idGoal - * @return array - */ - public function makeEmptyGoalRow($idGoal) + protected function getWhereStatement($tableName, $datetimeField, $extraWhere = false) { - if ($idGoal > Piwik_Tracker_GoalManager::IDGOAL_ORDER) { - return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - ); - } - if ($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER) { - return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, - ); + $where = "$tableName.$datetimeField >= ? + AND $tableName.$datetimeField <= ? + AND $tableName.idsite = ?"; + if (!empty($extraWhere)) { + $extraWhere = sprintf($extraWhere, $tableName, $tableName); + $where .= ' AND ' . $extraWhere; } - // idGoal == Piwik_Tracker_GoalManager::IDGOAL_CART - return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, - ); + return $where; } - /** - * Returns a Piwik_DataTable_Row containing default values for common stat, - * plus a column 'label' with the value $label - * - * @param string $label - * @return Piwik_DataTable_Row - */ - public function makeEmptyRowLabeled($label) + + protected function getSelectsMetrics($metricsAvailable, $metricsRequested = false) { - return new Piwik_DataTable_Row( - array( - Piwik_DataTable_Row::COLUMNS => array('label' => $label) - + $this->makeEmptyRow() - ) - ); + $selects = array(); + foreach ($metricsAvailable as $metricId => $statement) { + if ($this->isMetricRequested($metricId, $metricsRequested)) { + $selects[] = $statement . " as `" . $metricId . "`"; + } + } + return $selects; } /** - * Adds the given row $newRowToAdd to the existing $oldRowToUpdate passed by reference - * The rows are php arrays Name => value - * - * @param array $newRowToAdd - * @param array $oldRowToUpdate - * @param bool $onlyMetricsAvailableInActionsTable - * @param bool $doNotSumVisits - * - * @return void + * @param $metricId + * @param $metricsRequested + * @return bool */ - public function sumMetrics($newRowToAdd, &$oldRowToUpdate, $onlyMetricsAvailableInActionsTable = false) + protected function isMetricRequested($metricId, $metricsRequested) { - // Pre 1.2 format: string indexed rows are returned from the DB - // Left here for Backward compatibility with plugins doing custom SQL queries using these metrics as string - if (!isset($newRowToAdd[Piwik_Archive::INDEX_NB_VISITS])) { - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd['nb_visits']; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions']; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors']; - if ($onlyMetricsAvailableInActionsTable) { - return; - } - $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd['max_actions'], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); - $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd['sum_visit_length']; - $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd['bounce_count']; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd['nb_visits_converted']; - return; - } - - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS]; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd[Piwik_Archive::INDEX_NB_ACTIONS]; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Piwik_Archive::INDEX_NB_UNIQ_VISITORS]; - if ($onlyMetricsAvailableInActionsTable) { - return; - } - - $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd[Piwik_Archive::INDEX_MAX_ACTIONS], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); - $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd[Piwik_Archive::INDEX_SUM_VISIT_LENGTH]; - $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd[Piwik_Archive::INDEX_BOUNCE_COUNT]; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]; - + return $metricsRequested === false + || in_array($metricId, $metricsRequested); } /** - * Given an array of stats, it will process the sum of goal conversions - * and sum of revenue and add it in the stats array in two new fields. - * - * @param array $metricsByLabel Passed by reference, it will be modified as follows: - * Input: - * array( - * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, - * Piwik_Archive::INDEX_GOALS => array( - * idgoal1 => array( [...] ), - * idgoal2 => array( [...] ), - * ), - * [...] ), - * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) - * ); + * Returns the actions by the given dimension * + * - The basic use case is to use $dimensionRecord and optionally $where. + * - If you want to apply a limit and group the others, use $orderBy to sort the way you + * want the limit to be applied and pass a pre-configured instance of Piwik_RankingQuery. + * The ranking query instance has to have a limit and at least one label column. + * See Piwik_RankingQuery::setLimit() and Piwik_RankingQuery::addLabelColumn(). + * If $rankingQuery is set, the return value is the array returned by + * Piwik_RankingQuery::execute(). + * - By default, the method only queries log_link_visit_action. If you need data from + * log_action (e.g. to partition the result from the ranking query into the different + * action types), use $joinLogActionOnColumn and $additionalSelects to join log_action and select + * the column you need from log_action. * - * Output: - * array( - * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, - * Piwik_Archive::INDEX_NB_CONVERSIONS => Y, // sum of all conversions - * Piwik_Archive::INDEX_REVENUE => Z, // sum of all revenue - * Piwik_Archive::INDEX_GOALS => array( - * idgoal1 => array( [...] ), - * idgoal2 => array( [...] ), - * ), - * [...] ), - * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) - * ); - * ) * - * @param array $metricsByLabel Passed by reference, will be modified + * @param array|string $dimensions the dimensionRecord(s) you're interested in + * @param string $where where clause + * @param bool|string $additionalSelects additional select clause + * @param bool|array $metrics Set this if you want to limit the columns that are returned. + * The possible values in the array are Piwik_Archive::INDEX_*. + * @param Piwik_RankingQuery $rankingQuery pre-configured ranking query instance + * @param bool|string $joinLogActionOnColumn column from log_link_visit_action that + * log_action should be joined on. + * can be an array to join multiple times. + * @internal param bool|string $orderBy order by clause + * @return mixed */ - function enrichMetricsWithConversions(&$metricsByLabel) + protected function getGroupByStatement($dimensions, $tableName) { - foreach ($metricsByLabel as $label => &$values) { - if (isset($values[Piwik_Archive::INDEX_GOALS])) { - // When per goal metrics are processed, general 'visits converted' is not meaningful because - // it could differ from the sum of each goal conversions - unset($values[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]); - $revenue = $conversions = 0; - foreach ($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues) { - // Do not sum Cart revenue since it is a lost revenue - if ($idgoal >= Piwik_Tracker_GoalManager::IDGOAL_ORDER) { - $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE]; - $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; - } - } - $values[Piwik_Archive::INDEX_NB_CONVERSIONS] = $conversions; + $dimensions = $this->getSelectDimensions($dimensions, $tableName, $appendSelectAs = false); + $groupBy = implode(", ", $dimensions); + return $groupBy; + } - // 25.00 recorded as 25 - if (round($revenue) == $revenue) { - $revenue = round($revenue); - } - $values[Piwik_Archive::INDEX_REVENUE] = $revenue; + protected function getSelectDimensions($dimensions, $tableName, $appendSelectAs = true) + { + foreach ($dimensions as $selectAs => &$field) { + $selectAsString = $field; + if(!is_numeric($selectAs)) { + $selectAsString = $selectAs; + } + if($selectAsString == $field) { + $field = "$tableName.$field"; + } + if($appendSelectAs) { + $field = "$field AS $selectAsString"; } } + return $dimensions; } /** - * - * @param array $metricsByLabelAndSubLabel Passed by reference, will be modified + * Main method to process logs for a day. The only logic done here is computing the number of visits, actions, etc. + * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Day.compute'. + * See some of the plugins for an example eg. 'Provider' */ - function enrichPivotMetricsWithConversions(&$metricsByLabelAndSubLabel) + protected function compute() { - foreach ($metricsByLabelAndSubLabel as $mainLabel => &$metricsBySubLabel) { - $this->enrichMetricsWithConversions($metricsBySubLabel); + if (!$this->isThereSomeVisits()) { + return; } + Piwik_PostEvent('ArchiveProcessing_Day.compute', $this); } /** + * If a segment is specified but a plugin other than 'VisitsSummary' is being requested, + * we create an archive for processing VisitsSummary Core Metrics, which will in turn + * execute the query above (in isThereSomeVisits) * - * @param $newRowToAdd - * @param $oldRowToUpdate + * @return bool|null */ - function sumGoalMetrics($newRowToAdd, &$oldRowToUpdate) + private function makeArchiveToCheckForVisits() { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_REVENUE] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_REVENUE]; - - // Cart & Order - if (isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS])) { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS]; - - // Order only - if (isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL])) { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT]; - } + $archive = $this->makeNewArchive(); + $nbVisits = $archive->getNumeric('nb_visits'); + $this->isThereSomeVisits = $nbVisits > 0; + + if ($this->isThereSomeVisits) { + $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); + $this->setNumberOfVisits($nbVisits); + $this->setNumberOfVisitsConverted($nbVisitsConverted); } + + return $this->isThereSomeVisits; } + public function getDb() + { + return Zend_Registry::get('db'); + } } diff --git a/core/DataAccess/LogAggregator.php b/core/DataAccess/LogAggregator.php index 0e9efcc99c..aafc3e4c1c 100644 --- a/core/DataAccess/LogAggregator.php +++ b/core/DataAccess/LogAggregator.php @@ -21,10 +21,20 @@ class Piwik_DataAccess_LogAggregator * ie (AND, OR, etc.). * @return array An array of SQL SELECT expressions. */ - public static function buildReduceByRangeSelect( $column, $ranges, $table, $selectColumnPrefix = '', $extraCondition = false) + public static function getSelectsFromRangedColumn( $metadata ) { - $selects = array(); + @list($column, $ranges, $table, $selectColumnPrefix, $i_am_your_nightmare_DELETE_ME) = $metadata; + $selects = array(); + $extraCondition = ''; + if($i_am_your_nightmare_DELETE_ME) { + // extra condition for the SQL SELECT that makes sure only returning visits are counted + // when creating the 'days since last visit' report + $extraCondition = 'and log_visit.visitor_returning = 1'; + $extraSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) " + . " as `". $selectColumnPrefix . 'General_NewVisits' . "`"; + $selects[] = $extraSelect; + } foreach ($ranges as $gap) { if (count($gap) == 2) { $lowerBound = $gap[0]; @@ -46,4 +56,30 @@ class Piwik_DataAccess_LogAggregator return $selects; } + /** + * Clean up the row data and return values. + * $lookForThisPrefix can be used to make sure only SOME of the data in $row is used. + * + * The array will have one column $columnName + * + * @param $row + * @param $columnName + * @param $lookForThisPrefix A string that identifies which elements of $row to use + * in the result. Every key of $row that starts with this + * value is used. + * @return array + */ + static public function makeArrayOneColumn($row, $columnName, $lookForThisPrefix = false) + { + $cleanRow = array(); + foreach ($row as $label => $count) { + if (empty($lookForThisPrefix) + || strpos($label, $lookForThisPrefix) === 0) { + $cleanLabel = substr($label, strlen($lookForThisPrefix)); + $cleanRow[$cleanLabel] = array($columnName => $count); + } + } + return $cleanRow; + } + } \ No newline at end of file diff --git a/core/DataTable.php b/core/DataTable.php index ca38047bd9..8d7f75245f 100644 --- a/core/DataTable.php +++ b/core/DataTable.php @@ -1029,6 +1029,7 @@ class Piwik_DataTable // we then serialize the rows and store them in the serialized dataTable $addToRows = array(self::ID_SUMMARY_ROW => $this->summaryRow); + //FIXMEA let's kill this soon * re-do if necessary if ($this->parents && Piwik_Config::getInstance()->General['enable_archive_parents_of_datatable']) { $addToRows[self::ID_PARENTS] = $this->parents; } @@ -1181,7 +1182,7 @@ class Piwik_DataTable * LABEL => array(col1 => X, col2 => Y), * LABEL2 => array(col1 => X, col2 => Y), * ) - * to the structure + * to a DataTable, ie. with the internal structure * array ( * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)), * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)), @@ -1192,57 +1193,42 @@ class Piwik_DataTable * LABEL => X, * LABEL2 => Y, * ) - * would be converted to the structure + * would be converted to: * array ( * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, 'value' => X)), * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, 'value' => Y)), * ) * - * The optional parameter $subtablePerLabel is an array of subTable associated to the rows of the $array - * For example if $subtablePerLabel is given - * array( - * LABEL => #Piwik_DataTable_ForLABEL, - * LABEL2 => #Piwik_DataTable_ForLABEL2, - * ) - * - * the $array would become - * array ( - * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y), - * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID DataTable For LABEL - * ), - * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y) - * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID2 DataTable For LABEL2 - * ), - * ) * - * @param array $array See method description - * @param array|null $subtablePerLabel See method description + * @param array $array Indexed array, two formats are supported + * @param array|null $subtablePerLabel An indexed array of up to one DataTable to associate as a sub table */ - public function addRowsFromArrayWithIndexLabel($array, $subtablePerLabel = null) + public static function makeFromIndexedArray($array, $subtablePerLabel = null) { + $table = new Piwik_DataTable(); $cleanRow = array(); foreach ($array as $label => $row) { + // Support the case of an $array of single values if (!is_array($row)) { $row = array('value' => $row); } - $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = null; - // we put the 'label' column first as it looks prettier in API results + // Put the 'label' column first $cleanRow[Piwik_DataTable_Row::COLUMNS] = array('label' => $label) + $row; - if (!is_null($subtablePerLabel) - // some rows of this table don't have subtables - // (for example case of campaigns without keywords) - && isset($subtablePerLabel[$label]) - ) { + // Assign subtable if specified + if (isset($subtablePerLabel[$label])) { $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label]; } - $this->addRow(new Piwik_DataTable_Row($cleanRow)); + $table->addRow(new Piwik_DataTable_Row($cleanRow)); } + return $table; } /** * Set the array of parent ids * * @param array $parents + * + * FIXMEA */ public function setParents($parents) { diff --git a/core/DataTable/Row.php b/core/DataTable/Row.php index 5179618bfe..9038aed608 100644 --- a/core/DataTable/Row.php +++ b/core/DataTable/Row.php @@ -522,7 +522,7 @@ class Piwik_DataTable_Row ) { // We shall update metadata, and keep the metadata with the _most visits or pageviews_, rather than first or last seen $visits = max($rowToSum->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS) || $rowToSum->getColumn(Piwik_Archive::INDEX_NB_VISITS), - // Old format pre-1.2, @see also method sumMetrics() + // Old format pre-1.2, @see also method doSumVisitsMetrics() $rowToSum->getColumn('nb_actions') || $rowToSum->getColumn('nb_visits')); if (($visits && $visits > $this->maxVisitsSummed) || empty($this->c[self::METADATA]) diff --git a/core/PluginsArchiver.php b/core/PluginsArchiver.php index 7dcdf3e54f..dca85a7c77 100644 --- a/core/PluginsArchiver.php +++ b/core/PluginsArchiver.php @@ -19,7 +19,6 @@ abstract class Piwik_PluginsArchiver public function __construct(Piwik_ArchiveProcessing $processing) { - $this->maximumRows = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; $this->maximumRows = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; $this->processor = $processing; } diff --git a/core/Segment.php b/core/Segment.php index ce37845e88..b59e04d37f 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -162,8 +162,6 @@ class Piwik_Segment */ public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false) { - $joinWithSubSelect = false; - if (!is_array($from)) { $from = array($from); } diff --git a/plugins/Actions/Archiver.php b/plugins/Actions/Archiver.php index 84dc0b7da3..1a8e902f16 100644 --- a/plugins/Actions/Archiver.php +++ b/plugins/Actions/Archiver.php @@ -439,10 +439,14 @@ class Piwik_Actions_Archiver extends Piwik_PluginsArchiver { $dataTable = $this->getDataTable(Piwik_Tracker_Action::TYPE_ACTION_URL); $this->recordDataTable($dataTable, self::PAGE_URLS_RECORD_NAME); - $this->getProcessor()->insertNumericRecord(self::METRIC_PAGEVIEWS_RECORD_NAME, array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS))); - $this->getProcessor()->insertNumericRecord(self::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME, array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS))); - $this->getProcessor()->insertNumericRecord(self::METRIC_SUM_TIME_RECORD_NAME, array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION))); - $this->getProcessor()->insertNumericRecord(self::METRIC_HITS_TIMED_RECORD_NAME, array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION))); + + $records = array( + self::METRIC_PAGEVIEWS_RECORD_NAME => array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS)), + self::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME => array_sum($dataTable->getColumn(Piwik_Archive::INDEX_NB_VISITS)), + self::METRIC_SUM_TIME_RECORD_NAME => array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION)), + self::METRIC_HITS_TIMED_RECORD_NAME => array_sum($dataTable->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION)) + ); + $this->getProcessor()->insertNumericRecords( $records ); } /** diff --git a/plugins/CustomVariables/Archiver.php b/plugins/CustomVariables/Archiver.php index f5df35ba38..15cc2fdf23 100644 --- a/plugins/CustomVariables/Archiver.php +++ b/plugins/CustomVariables/Archiver.php @@ -13,8 +13,11 @@ class Piwik_CustomVariables_Archiver extends Piwik_PluginsArchiver { const LABEL_CUSTOM_VALUE_NOT_DEFINED = "Value not defined"; const CUSTOM_VARIABLE_RECORD_NAME = 'CustomVariables_valueByName'; - protected $metricsByKey = array(); - protected $metricsByKeyAndValue = array(); + + /** + * @var Piwik_DataArray + */ + protected $dataArray; protected $maximumRowsInDataTableLevelZero; protected $maximumRowsInSubDataTable; protected $newEmptyRow; @@ -28,15 +31,15 @@ class Piwik_CustomVariables_Archiver extends Piwik_PluginsArchiver public function archiveDay() { + $this->dataArray = new Piwik_DataArray(); + for ($i = 1; $i <= Piwik_Tracker::MAX_CUSTOM_VARIABLES; $i++) { $this->aggregateCustomVariable($i); } $this->removeVisitsMetricsFromActionsAggregate(); - $this->getProcessor()->enrichMetricsWithConversions($this->metricsByKey); - $this->getProcessor()->enrichPivotMetricsWithConversions($this->metricsByKeyAndValue); - - $table = $this->getProcessor()->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByKeyAndValue, $this->metricsByKey); + $this->dataArray->enrichMetricsWithConversions(); + $table = $this->getProcessor()->getDataTableFromDataArray($this->dataArray); $blob = $table->getSerialized( $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $columnToSort = Piwik_Archive::INDEX_NB_VISITS @@ -55,21 +58,34 @@ class Piwik_CustomVariables_Archiver extends Piwik_PluginsArchiver $query = $this->getProcessor()->queryVisitsByDimension($dimensions, $where); $this->aggregateFromVisits($query, $keyField, $valueField); - $query = $this->getProcessor()->queryActionsByDimension($dimensions, $where); + // IF we query Custom Variables scope "page" either: Product SKU, Product Name, + // then we also query the "Product page view" price which was possibly recorded. + $additionalSelects = false; + // FIXMEA + if (in_array($slot, array(3,4,5))) { + $additionalSelects = array( $this->getSelectAveragePrice() ); + } + $query = $this->getProcessor()->queryActionsByDimension($dimensions, $where, $additionalSelects); $this->aggregateFromActions($query, $keyField, $valueField); $query = $this->getProcessor()->queryConversionsByDimension($dimensions, $where); $this->aggregateFromConversions($query, $keyField, $valueField); } + protected function getSelectAveragePrice() + { + return Piwik_ArchiveProcessing_Day::getSqlRevenue("AVG(log_link_visit_action.custom_var_v2)") + . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED . "`"; + } + protected function aggregateFromVisits($query, $keyField, $valueField) { while ($row = $query->fetch()) { - $value = $row[$valueField]; - $value = $this->cleanCustomVarValue($value); - $key = $row[$keyField]; - $this->aggregateVisit($key, $value, $row); + $value = $this->cleanCustomVarValue($row[$valueField]); + + $this->dataArray->sumMetricsVisits($key, $row); + $this->dataArray->sumMetricsVisitsPivot($key, $value, $row); } } @@ -81,35 +97,18 @@ class Piwik_CustomVariables_Archiver extends Piwik_PluginsArchiver return self::LABEL_CUSTOM_VALUE_NOT_DEFINED; } - protected function aggregateVisit($key, $value, $row) - { - if (!isset($this->metricsByKey[$key])) { - $this->metricsByKey[$key] = $this->getProcessor()->makeEmptyRow(); - } - if (!isset($this->metricsByKeyAndValue[$key][$value])) { - $this->metricsByKeyAndValue[$key][$value] = $this->getProcessor()->makeEmptyRow(); - } - - $this->getProcessor()->sumMetrics($row, $this->metricsByKey[$key]); - $this->getProcessor()->sumMetrics($row, $this->metricsByKeyAndValue[$key][$value]); - } protected function aggregateFromActions($query, $keyField, $valueField) { while ($row = $query->fetch()) { $key = $row[$keyField]; - $value = $row[$valueField]; - $value = $this->cleanCustomVarValue($value); - $this->aggregateAction($key, $value, $row); - } - } + $value = $this->cleanCustomVarValue($row[$valueField]); - protected function aggregateAction($key, $value, $row) - { - $alreadyAggregated = $this->aggregateEcommerceCategories($key, $value, $row); - if (!$alreadyAggregated) { - $this->aggregateActionByKeyAndValue($key, $value, $row); - $this->aggregateActionByKey($key, $row); + $alreadyAggregated = $this->aggregateEcommerceCategories($key, $value, $row); + if (!$alreadyAggregated) { + $this->aggregateActionByKeyAndValue($key, $value, $row); + $this->dataArray->sumMetricsActions($key, $row); + } } } @@ -146,17 +145,14 @@ class Piwik_CustomVariables_Archiver extends Piwik_PluginsArchiver protected function aggregateActionByKeyAndValue($key, $value, $row) { - if (!isset($this->metricsByKeyAndValue[$key][$value])) { - $this->metricsByKeyAndValue[$key][$value] = $this->getProcessor()->makeEmptyActionRow(); - } - $this->getProcessor()->sumMetrics($row, $this->metricsByKeyAndValue[$key][$value], $onlyMetricsAvailableInActionsTable = true); + $this->dataArray->sumMetricsActionsPivot($key, $value, $row); if ($this->isReservedKey($key)) { // Price tracking on Ecommerce product/category pages: // the average is returned from the SQL query so the price is not "summed" like other metrics $index = Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED; if (!empty($row[$index])) { - $this->metricsByKeyAndValue[$key][$value][$index] = (float)$row[$index]; + $this->dataArray->setRowColumnPivot($key, $value, $index, (float)$row[$index]); } } } @@ -166,13 +162,6 @@ class Piwik_CustomVariables_Archiver extends Piwik_PluginsArchiver return in_array($key, Piwik_CustomVariables_API::getReservedCustomVariableKeys()); } - protected function aggregateActionByKey($key, $row) - { - if (!isset($this->metricsByKey[$key])) { - $this->metricsByKey[$key] = $this->getProcessor()->makeEmptyActionRow(); - } - $this->getProcessor()->sumMetrics($row, $this->metricsByKey[$key], $onlyMetricsAvailableInActionsTable = true); - } protected function aggregateFromConversions($query, $keyField, $valueField) { @@ -182,32 +171,17 @@ class Piwik_CustomVariables_Archiver extends Piwik_PluginsArchiver while ($row = $query->fetch()) { $key = $row[$keyField]; $value = $this->cleanCustomVarValue($row[$valueField]); - $idGoal = $row['idgoal']; - $this->aggregateConversion($key, $value, $idGoal, $row); - } - } - - protected function aggregateConversion($key, $value, $idGoal, $row) - { - if (!isset($this->metricsByKey[$key][Piwik_Archive::INDEX_GOALS][$idGoal])) { - $this->metricsByKey[$key][Piwik_Archive::INDEX_GOALS][$idGoal] = $this->getProcessor()->makeEmptyGoalRow($idGoal); - } - if (!isset($this->metricsByKeyAndValue[$key][$value][Piwik_Archive::INDEX_GOALS][$idGoal])) { - $this->metricsByKeyAndValue[$key][$value][Piwik_Archive::INDEX_GOALS][$idGoal] = $this->getProcessor()->makeEmptyGoalRow($idGoal); + $this->dataArray->sumMetricsGoals($key, $row); + $this->dataArray->sumMetricsGoalsPivot($key, $value, $row); } - - $this->getProcessor()->sumGoalMetrics($row, $this->metricsByKey[$key][Piwik_Archive::INDEX_GOALS][$idGoal]); - $this->getProcessor()->sumGoalMetrics($row, $this->metricsByKeyAndValue[$key][$value][Piwik_Archive::INDEX_GOALS][$idGoal]); } protected function removeVisitsMetricsFromActionsAggregate() { - $emptyActionRow = $this->getProcessor()->makeEmptyActionRow(); - - foreach ($this->metricsByKey as $key => &$row) { - $isActionRowAggregate = (count($row) == count($emptyActionRow)); + $dataArray = &$this->dataArray->getDataArray(); + foreach ($dataArray as $key => &$row) { if (!self::isReservedKey($key) - && $isActionRowAggregate + && Piwik_DataArray::isRowActions($row) ) { unset($row[Piwik_Archive::INDEX_NB_UNIQ_VISITORS]); unset($row[Piwik_Archive::INDEX_NB_VISITS]); diff --git a/plugins/DBStats/API.php b/plugins/DBStats/API.php index fec0e0fe71..693c7f2e1e 100644 --- a/plugins/DBStats/API.php +++ b/plugins/DBStats/API.php @@ -128,9 +128,7 @@ class Piwik_DBStats_API $rowToAddTo['row_count'] += $status['Rows']; } - $result = new Piwik_DataTable(); - $result->addRowsFromArrayWithIndexLabel($rows); - return $result; + return Piwik_DataTable::makeFromIndexedArray($rows); } /** diff --git a/plugins/DevicesDetection/Archiver.php b/plugins/DevicesDetection/Archiver.php index 5ff05d3f3a..8bedcc9d39 100644 --- a/plugins/DevicesDetection/Archiver.php +++ b/plugins/DevicesDetection/Archiver.php @@ -11,82 +11,46 @@ class Piwik_DevicesDetection_Archiver extends Piwik_PluginsArchiver { - const TYPE_RECORD_NAME = 'DevicesDetection_types'; - const BRAND_RECORD_NAME = 'DevicesDetection_brands'; - const MODEL_RECORD_NAME = 'DevicesDetection_models'; + const DEVICE_TYPE_RECORD_NAME = 'DevicesDetection_types'; + const DEVICE_BRAND_RECORD_NAME = 'DevicesDetection_brands'; + const DEVICE_MODEL_RECORD_NAME = 'DevicesDetection_models'; const OS_RECORD_NAME = 'DevicesDetection_os'; const OS_VERSION_RECORD_NAME = 'DevicesDetection_osVersions'; const BROWSER_RECORD_NAME = 'DevicesDetection_browsers'; const BROWSER_VERSION_RECORD_NAME = 'DevicesDetection_browserVersions'; - public function __construct($processor) - { - parent::__construct($processor); - $this->maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; - } + const DEVICE_TYPE_FIELD = "config_device_type"; + const DEVICE_BRAND_FIELD = "config_device_brand"; + const DEVICE_MODEL_FIELD = "config_device_model"; + const OS_FIELD = "config_os"; + const OS_VERSION_FIELD = "CONCAT(log_visit.config_os, ';', log_visit.config_os_version)"; + const BROWSER_FIELD = "config_browser_name"; + const BROWSER_VERSION_DIMENSION = "CONCAT(log_visit.config_browser_name, ';', log_visit.config_browser_version)"; public function archiveDay() { - $this->archiveDayDeviceTypes(); - $this->archiveDayDeviceBrands(); - $this->archiveDayDeviceModels(); - $this->archiveDayOs(); - $this->archiveDayOsVersions(); - $this->archiveDayBrowserFamilies(); - $this->archiveDayBrowsersVersions(); - } - - private function archiveDayDeviceTypes() - { - $labelSQL = "log_visit.config_device_type"; - $this->aggregateByLabel( $labelSQL, self::TYPE_RECORD_NAME); + $this->aggregateByLabel( self::DEVICE_TYPE_FIELD, self::DEVICE_TYPE_RECORD_NAME); + $this->aggregateByLabel( self::DEVICE_BRAND_FIELD, self::DEVICE_BRAND_RECORD_NAME); + $this->aggregateByLabel( self::DEVICE_MODEL_FIELD, self::DEVICE_MODEL_RECORD_NAME); + $this->aggregateByLabel( self::OS_FIELD, self::OS_RECORD_NAME); + $this->aggregateByLabel( self::OS_VERSION_FIELD, self::OS_VERSION_RECORD_NAME); + $this->aggregateByLabel( self::BROWSER_FIELD, self::BROWSER_RECORD_NAME); + $this->aggregateByLabel( self::BROWSER_VERSION_DIMENSION, self::BROWSER_VERSION_RECORD_NAME); } private function aggregateByLabel( $labelSQL, $recordName) { - $metricsByLabel = $this->getProcessor()->getMetricsForLabel($labelSQL); - $tableBrand = $this->getProcessor()->getDataTableFromArray($metricsByLabel); - - $this->getProcessor()->insertBlobRecord($recordName, $tableBrand->getSerialized($this->maximumRowsInDataTable, null, Piwik_Archive::INDEX_NB_VISITS)); - } - - private function archiveDayDeviceBrands() - { - $this->aggregateByLabel( "log_visit.config_device_brand", self::BRAND_RECORD_NAME); - } - - private function archiveDayDeviceModels() - { - $this->aggregateByLabel( "log_visit.config_device_model", self::MODEL_RECORD_NAME); - } - - private function archiveDayOs() - { - $this->aggregateByLabel( "log_visit.config_os", self::OS_RECORD_NAME); - } - - private function archiveDayOsVersions() - { - $this->aggregateByLabel( "CONCAT(log_visit.config_os, ';', log_visit.config_os_version)", self::OS_VERSION_RECORD_NAME); - } - - private function archiveDayBrowserFamilies() - { - $this->aggregateByLabel( "log_visit.config_browser_name", self::BROWSER_RECORD_NAME); - } - - private function archiveDayBrowsersVersions() - { - $this->aggregateByLabel( "CONCAT(log_visit.config_browser_name, ';', log_visit.config_browser_version)", self::BROWSER_VERSION_RECORD_NAME); + $metrics = $this->getProcessor()->getMetricsForDimension($labelSQL); + $table = $this->getProcessor()->getDataTableFromDataArray($metrics); + $this->getProcessor()->insertBlobRecord($recordName, $table->getSerialized($this->maximumRows, null, Piwik_Archive::INDEX_NB_VISITS)); } public function archivePeriod() { - $maximumRowsInSubDataTable = $this->maximumRowsInDataTable; $dataTablesToSum = array( - self::TYPE_RECORD_NAME, - self::BRAND_RECORD_NAME, - self::MODEL_RECORD_NAME, + self::DEVICE_TYPE_RECORD_NAME, + self::DEVICE_BRAND_RECORD_NAME, + self::DEVICE_MODEL_RECORD_NAME, self::OS_RECORD_NAME, self::OS_VERSION_RECORD_NAME, self::BROWSER_RECORD_NAME, @@ -94,7 +58,7 @@ class Piwik_DevicesDetection_Archiver extends Piwik_PluginsArchiver ); foreach ($dataTablesToSum as $dt) { $this->getProcessor()->archiveDataTable( - $dt, null, $this->maximumRowsInDataTable, $maximumRowsInSubDataTable, $columnToSort = "nb_visits"); + $dt, null, $this->maximumRows, $this->maximumRows, $columnToSort = "nb_visits"); } } } \ No newline at end of file diff --git a/plugins/ExampleUI/API.php b/plugins/ExampleUI/API.php index 91e29806be..697844a589 100644 --- a/plugins/ExampleUI/API.php +++ b/plugins/ExampleUI/API.php @@ -50,11 +50,7 @@ class Piwik_ExampleUI_API $value = array('server1' => $server1, 'server2' => $server2); $temperatures[$subPeriod->getLocalizedShortString()] = $value; } - - // convert this array to a DataTable object - $dataTable = new Piwik_DataTable(); - $dataTable->addRowsFromArrayWithIndexLabel($temperatures); - return $dataTable; + return Piwik_DataTable::makeFromIndexedArray($temperatures); } // we generate an array of random server temperatures @@ -71,10 +67,7 @@ class Piwik_ExampleUI_API $temperatures[$xAxisLabel] = $temperatureValues[$i]; } - // convert this array to a DataTable object - $dataTable = new Piwik_DataTable(); - $dataTable->addRowsFromArrayWithIndexLabel($temperatures); - return $dataTable; + return Piwik_DataTable::makeFromIndexedArray($temperatures); } public function getPlanetRatios() @@ -90,9 +83,7 @@ class Piwik_ExampleUI_API 'Neptune' => 3.883, ); // convert this array to a DataTable object - $dataTable = new Piwik_DataTable(); - $dataTable->addRowsFromArrayWithIndexLabel($planetRatios); - return $dataTable; + return Piwik_DataTable::makeFromIndexedArray($planetRatios); } public function getPlanetRatiosWithLogos() diff --git a/plugins/Goals/Archiver.php b/plugins/Goals/Archiver.php index 45d915e1ab..0a5a154f3b 100644 --- a/plugins/Goals/Archiver.php +++ b/plugins/Goals/Archiver.php @@ -16,6 +16,17 @@ class Piwik_Goals_Archiver extends Piwik_PluginsArchiver const ITEMS_SKU_RECORD_NAME = 'Goals_ItemsSku'; const ITEMS_NAME_RECORD_NAME = 'Goals_ItemsName'; const ITEMS_CATEGORY_RECORD_NAME = 'Goals_ItemsCategory'; + const SKU_FIELD = 'idaction_sku'; + const NAME_FIELD = 'idaction_name'; + const CATEGORY_FIELD = 'idaction_category'; + const CATEGORY2_FIELD = 'idaction_category2'; + const CATEGORY3_FIELD = 'idaction_category3'; + const CATEGORY4_FIELD = 'idaction_category4'; + const CATEGORY5_FIELD = 'idaction_category5'; + const NO_LABEL = ':'; + const LOG_CONVERSION_TABLE = 'log_conversion'; + const VISITS_COUNT_FIELD = 'visitor_count_visits'; + const DAYS_SINCE_FIRST_VISIT_FIELD = 'visitor_days_since_first'; /** * This array stores the ranges to use when displaying the 'visits to conversion' report */ @@ -53,209 +64,307 @@ class Piwik_Goals_Archiver extends Piwik_PluginsArchiver array(121, 364), array(364) ); - - protected $dimensions = array( - 'idaction_sku' => self::ITEMS_SKU_RECORD_NAME, - 'idaction_name' => self::ITEMS_NAME_RECORD_NAME, - 'idaction_category' => self::ITEMS_CATEGORY_RECORD_NAME + protected $dimensionRecord = array( + self::SKU_FIELD => self::ITEMS_SKU_RECORD_NAME, + self::NAME_FIELD => self::ITEMS_NAME_RECORD_NAME, + self::CATEGORY_FIELD => self::ITEMS_CATEGORY_RECORD_NAME ); + /** + * Array containing one DataArray for each Ecommerce items dimension (name/sku/category abandoned carts and orders) + * @var array + */ + protected $itemReports = array(); + public function archiveDay() { $this->archiveGeneralGoalMetrics(); $this->archiveEcommerceItems(); } - function archiveGeneralGoalMetrics() + protected function archiveGeneralGoalMetrics() { - $selectAsVisitCount = 'vcv'; - $selectAsDaysSince = 'vdsf'; - // extra aggregate selects for the visits to conversion report - $visitToConvExtraCols = Piwik_DataAccess_LogAggregator::buildReduceByRangeSelect( - 'visitor_count_visits', self::$visitCountRanges, 'log_conversion', $selectAsVisitCount); - - // extra aggregate selects for the days to conversion report - $daysToConvExtraCols = Piwik_DataAccess_LogAggregator::buildReduceByRangeSelect( - 'visitor_days_since_first', self::$daysToConvRanges, 'log_conversion', $selectAsDaysSince); - - $query = $this->getProcessor()->queryConversionsByDimension(array(), '', array_merge($visitToConvExtraCols, $daysToConvExtraCols)); + $prefixes = array( + self::VISITS_UNTIL_RECORD_NAME => 'vcv', + self::DAYS_UNTIL_CONV_RECORD_NAME => 'vdsf', + ); + $aggregatesMetadata = array( + array(self::VISITS_COUNT_FIELD, self::$visitCountRanges, self::LOG_CONVERSION_TABLE, $prefixes[self::VISITS_UNTIL_RECORD_NAME]), + array(self::DAYS_SINCE_FIRST_VISIT_FIELD, self::$daysToConvRanges, self::LOG_CONVERSION_TABLE, $prefixes[self::DAYS_UNTIL_CONV_RECORD_NAME]), + ); + $selects = array(); + foreach ($aggregatesMetadata as $aggregateMetadata) { + $selects = array_merge($selects, Piwik_DataAccess_LogAggregator::getSelectsFromRangedColumn($aggregateMetadata)); + } + $query = $this->getProcessor()->queryConversionsByDimension(array(), false, $selects); if ($query === false) { return; } - $goals = array(); - $visitsToConvReport = array(); - $daysToConvReport = array(); + $totalConversions = $totalRevenue = 0; + $goals = new Piwik_DataArray(); + $visitsToConversions = $daysToConversions = array(); - // Get a standard empty goal row - $overall = $this->getProcessor()->makeEmptyGoalRow($idGoal = 1); + $conversionMetrics = $this->getProcessor()->getConversionsMetricFields(); while ($row = $query->fetch()) { - $idgoal = $row['idgoal']; + $idGoal = $row['idgoal']; + unset($row['idgoal']); + unset($row['label']); - if (!isset($goals[$idgoal])) { - $goals[$idgoal] = $this->getProcessor()->makeEmptyGoalRow($idgoal); + $values = array(); + foreach($conversionMetrics as $field => $statement) { + $values[$field] = $row[$field]; + } + $goals->sumMetrics($idGoal, $values); - $visitsToConvReport[$idgoal] = new Piwik_DataTable(); - $daysToConvReport[$idgoal] = new Piwik_DataTable(); + if (empty($visitsToConversions[$idGoal])) { + $visitsToConversions[$idGoal] = new Piwik_DataTable(); } - $this->getProcessor()->sumGoalMetrics($row, $goals[$idgoal]); + $array = Piwik_DataAccess_LogAggregator::makeArrayOneColumn($row, Piwik_Archive::INDEX_NB_CONVERSIONS, $prefixes[self::VISITS_UNTIL_RECORD_NAME]); + $visitsToConversions[$idGoal]->addDataTable(Piwik_DataTable::makeFromIndexedArray($array)); + + if (empty($daysToConversions[$idGoal])) { + $daysToConversions[$idGoal] = new Piwik_DataTable(); + } + $array = Piwik_DataAccess_LogAggregator::makeArrayOneColumn($row, Piwik_Archive::INDEX_NB_CONVERSIONS, $prefixes[self::DAYS_UNTIL_CONV_RECORD_NAME]); + $daysToConversions[$idGoal]->addDataTable(Piwik_DataTable::makeFromIndexedArray($array)); // We don't want to sum Abandoned cart metrics in the overall revenue/conversions/converted visits // since it is a "negative conversion" - if ($idgoal != Piwik_Tracker_GoalManager::IDGOAL_CART) { - $this->getProcessor()->sumGoalMetrics($row, $overall); + if ($idGoal != Piwik_Tracker_GoalManager::IDGOAL_CART) { + $totalConversions += $row[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; + $totalRevenue += $row[Piwik_Archive::INDEX_GOAL_REVENUE]; } - - // map the goal + visit number of a visitor with the # of conversions that happened on that visit - $table = $this->getProcessor()->getSimpleDataTableFromRow($row, Piwik_Archive::INDEX_NB_CONVERSIONS, $selectAsVisitCount); - $visitsToConvReport[$idgoal]->addDataTable($table); - - // map the goal + day number of a visit with the # of conversion that happened on that day - $table = $this->getProcessor()->getSimpleDataTableFromRow($row, Piwik_Archive::INDEX_NB_CONVERSIONS, $selectAsDaysSince); - $daysToConvReport[$idgoal]->addDataTable($table); } - // these data tables hold reports for every goal of a site - $visitsToConvOverview = new Piwik_DataTable(); - $daysToConvOverview = new Piwik_DataTable(); - // Stats by goal, for all visitors - foreach ($goals as $idgoal => $values) { - foreach ($values as $metricId => $value) { - $metricName = Piwik_Archive::$mappingFromIdToNameGoal[$metricId]; - $recordName = self::getRecordName($metricName, $idgoal); - $this->getProcessor()->insertNumericRecord($recordName, $value); - } - $conversion_rate = $this->getConversionRate($values[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED]); - $recordName = self::getRecordName('conversion_rate', $idgoal); - $this->getProcessor()->insertNumericRecord($recordName, $conversion_rate); - - // if the goal is not a special goal (like ecommerce) add it to the overview report - if ($idgoal !== Piwik_Tracker_GoalManager::IDGOAL_CART && - $idgoal !== Piwik_Tracker_GoalManager::IDGOAL_ORDER - ) { - $visitsToConvOverview->addDataTable($visitsToConvReport[$idgoal]); - $daysToConvOverview->addDataTable($daysToConvReport[$idgoal]); - } - - // visit count until conversion stats - $this->getProcessor()->insertBlobRecord( - self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $idgoal), - $visitsToConvReport[$idgoal]->getSerialized()); + $numericRecords = $this->getConversionsNumericMetrics($goals); + $this->getProcessor()->insertNumericRecords($numericRecords); - // day count until conversion stats - $this->getProcessor()->insertBlobRecord( - self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $idgoal), - $daysToConvReport[$idgoal]->getSerialized()); - } - - // archive overview reports - $this->getProcessor()->insertBlobRecord( - self::getRecordName(self::VISITS_UNTIL_RECORD_NAME), $visitsToConvOverview->getSerialized()); - $this->getProcessor()->insertBlobRecord( - self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME), $daysToConvOverview->getSerialized()); + $this->insertReports(self::VISITS_UNTIL_RECORD_NAME, $visitsToConversions); + $this->insertReports(self::DAYS_UNTIL_CONV_RECORD_NAME, $daysToConversions); // Stats for all goals - $totalAllGoals = array( + $metrics = array( self::getRecordName('conversion_rate') => $this->getConversionRate($this->getProcessor()->getNumberOfVisitsConverted()), - self::getRecordName('nb_conversions') => $overall[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS], + self::getRecordName('nb_conversions') => $totalConversions, self::getRecordName('nb_visits_converted') => $this->getProcessor()->getNumberOfVisitsConverted(), - self::getRecordName('revenue') => $overall[Piwik_Archive::INDEX_GOAL_REVENUE], + self::getRecordName('revenue') => $totalRevenue, ); - foreach ($totalAllGoals as $recordName => $value) { - $this->getProcessor()->insertNumericRecord($recordName, $value); + $this->getProcessor()->insertNumericRecords($metrics); + } + + protected function getConversionsNumericMetrics(Piwik_DataArray $goals) + { + $numericRecords = array(); + $goals = $goals->getDataArray(); + foreach ($goals as $idGoal => $array) { + foreach ($array as $metricId => $value) { + $metricName = Piwik_Archive::$mappingFromIdToNameGoal[$metricId]; + $recordName = self::getRecordName($metricName, $idGoal); + $numericRecords[$recordName] = $value; + } + if(!empty($array[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED])) { + $conversion_rate = $this->getConversionRate($array[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED]); + $recordName = self::getRecordName('conversion_rate', $idGoal); + $numericRecords[$recordName] = $conversion_rate; + } } + return $numericRecords; } /** - * @param Piwik_ArchiveProcessing_Day $this->getProcessor() + * @param string $recordName 'nb_conversions' + * @param int|bool $idGoal idGoal to return the metrics for, or false to return overall + * @return string Archive record name */ - function archiveEcommerceItems() + static public function getRecordName($recordName, $idGoal = false) { - if (!$this->shouldArchiveEcommerceItems()) { - return false; + $idGoalStr = ''; + if ($idGoal !== false) { + $idGoalStr = $idGoal . "_"; } - $items = array(); + return 'Goal_' . $idGoalStr . $recordName; + } - $dimensionsToQuery = $this->dimensions; - $dimensionsToQuery['idaction_category2'] = 'AdditionalCategory'; - $dimensionsToQuery['idaction_category3'] = 'AdditionalCategory'; - $dimensionsToQuery['idaction_category4'] = 'AdditionalCategory'; - $dimensionsToQuery['idaction_category5'] = 'AdditionalCategory'; + protected function getConversionRate($count) + { + $visits = $this->getProcessor()->getNumberOfVisits(); + return round(100 * $count / $visits, Piwik_Tracker_GoalManager::REVENUE_PRECISION); + } + + protected function insertReports($recordName, $visitsToConversions) + { + foreach ($visitsToConversions as $idGoal => $table) { + $record = self::getRecordName($recordName, $idGoal); + $this->getProcessor()->insertBlobRecord($record, $table->getSerialized()); + } + $overviewTable = $this->getOverviewFromGoalTables($visitsToConversions); + $this->getProcessor()->insertBlobRecord(self::getRecordName($recordName), $overviewTable->getSerialized()); + } + + protected function getOverviewFromGoalTables($tableByGoal) + { + $overview = new Piwik_DataTable(); + foreach ($tableByGoal as $idGoal => $table) { + if ($this->isStandardGoal($idGoal)) { + $overview->addDataTable($table); + } + } + return $overview; + } - foreach ($dimensionsToQuery as $dimension => $recordName) { + protected function isStandardGoal($idGoal) + { + return !in_array($idGoal, $this->getEcommerceIdGoals()); + } + + protected function archiveEcommerceItems() + { + if (!$this->shouldArchiveEcommerceItems()) { + return false; + } + $this->initItemReports(); + foreach ($this->getItemsDimensions() as $dimension) { $query = $this->getProcessor()->queryEcommerceItems($dimension); if ($query == false) { continue; } + $this->aggregateFromEcommerceItems($query, $dimension); + } + $this->recordItemReports(); + } - while ($row = $query->fetch()) { - $label = $row['label']; - $ecommerceType = $row['ecommerceType']; - - if (empty($label)) { - // idaction==0 case: - // If we are querying any optional category, we do not include idaction=0 - // Otherwise we over-report in the Product Categories report - if ($recordName == 'AdditionalCategory') { - continue; - } - // Product Name/Category not defined" - if (class_exists('Piwik_CustomVariables')) { - $label = Piwik_CustomVariables_Archiver::LABEL_CUSTOM_VALUE_NOT_DEFINED; - } else { - $label = "Value not defined"; - } - } - // For carts, idorder = 0. To count abandoned carts, we must count visits with an abandoned cart - if ($ecommerceType == Piwik_Tracker_GoalManager::IDGOAL_CART) { - $row[Piwik_Archive::INDEX_ECOMMERCE_ORDERS] = $row[Piwik_Archive::INDEX_NB_VISITS]; - } - unset($row[Piwik_Archive::INDEX_NB_VISITS]); - unset($row['label']); - unset($row['ecommerceType']); - - $columnsToRound = array( - Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE, - Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY, - Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE, - Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED, - ); - foreach ($columnsToRound as $column) { - if (isset($row[$column]) - && $row[$column] == round($row[$column]) - ) { - $row[$column] = round($row[$column]); - } - } - $items[$dimension][$ecommerceType][$label] = $row; + protected function initItemReports() + { + foreach ($this->getEcommerceIdGoals() as $ecommerceType) { + foreach ($this->dimensionRecord as $dimension => $record) { + $this->itemReports[$dimension][$ecommerceType] = new Piwik_DataArray(); } } + } - foreach ($this->dimensions as $dimension => $recordName) { - foreach (array(Piwik_Tracker_GoalManager::IDGOAL_CART, Piwik_Tracker_GoalManager::IDGOAL_ORDER) as $ecommerceType) { - if (!isset($items[$dimension][$ecommerceType])) { - continue; - } - $recordNameInsert = $recordName; + protected function recordItemReports() + { + /** @var Piwik_DataArray $array */ + foreach ($this->itemReports as $dimension => $itemAggregatesByType) { + foreach ($itemAggregatesByType as $ecommerceType => $itemAggregate) { + $recordName = $this->dimensionRecord[$dimension]; if ($ecommerceType == Piwik_Tracker_GoalManager::IDGOAL_CART) { - $recordNameInsert = self::getItemRecordNameAbandonedCart($recordName); - } - $table = $this->getProcessor()->getDataTableFromArray($items[$dimension][$ecommerceType]); - - // For "category" report, we aggregate all 5 category queries into one datatable - if ($dimension == 'idaction_category') { - foreach (array('idaction_category2', 'idaction_category3', 'idaction_category4', 'idaction_category5') as $categoryToSum) { - if (!empty($items[$categoryToSum][$ecommerceType])) { - $tableToSum = $this->getProcessor()->getDataTableFromArray($items[$categoryToSum][$ecommerceType]); - $table->addDataTable($tableToSum); - } - } + $recordName = self::getItemRecordNameAbandonedCart($recordName); } - $this->getProcessor()->insertBlobRecord($recordNameInsert, $table->getSerialized()); + $table = $this->getProcessor()->getDataTableFromDataArray($itemAggregate); + $this->getProcessor()->insertBlobRecord($recordName, $table->getSerialized()); + } + } + } + + protected function shouldArchiveEcommerceItems() + { + // Per item doesn't support segment + // Also, when querying Goal metrics for visitorType==returning, we wouldnt want to trigger an extra request + // event if it did support segment + // (when this is implemented, we should have shouldProcessReportsForPlugin() support partial archiving based on which metric is requested) + if (!$this->getProcessor()->getSegment()->isEmpty()) { + return false; + } + return true; + } + + protected function getItemsDimensions() + { + $dimensions = array_keys($this->dimensionRecord); + foreach ($this->getItemExtraCategories() as $category) { + $dimensions[] = $category; + } + return $dimensions; + } + + protected function getItemExtraCategories() + { + return array(self::CATEGORY2_FIELD, self::CATEGORY3_FIELD, self::CATEGORY4_FIELD, self::CATEGORY5_FIELD); + } + + protected function isItemExtraCategory($field) + { + return in_array($field, $this->getItemExtraCategories()); + } + + protected function aggregateFromEcommerceItems($query, $dimension) + { + while ($row = $query->fetch()) { + $ecommerceType = $row['ecommerceType']; + + $label = $this->cleanupRowGetLabel($row, $dimension); + if ($label === false) { + continue; + } + + // Aggregate extra categories in the Item categories array + if ($this->isItemExtraCategory($dimension)) { + $array = $this->itemReports[self::CATEGORY_FIELD][$ecommerceType]; + } else { + $array = $this->itemReports[$dimension][$ecommerceType]; + } + + $this->roundColumnValues($row); + $array->sumMetrics($label, $row); + } + } + + protected function cleanupRowGetLabel(&$row, $currentField) + { + $label = $row['label']; + if (empty($label)) { + // An empty additional category -> skip this iteration + if ($this->isItemExtraCategory($currentField)) { + return false; + } + $label = "Value not defined"; + // Product Name/Category not defined" + if (class_exists('Piwik_CustomVariables')) { + $label = Piwik_CustomVariables_Archiver::LABEL_CUSTOM_VALUE_NOT_DEFINED; } } + + if ($row['ecommerceType'] == Piwik_Tracker_GoalManager::IDGOAL_CART) { + // abandoned carts are the numner of visits with an abandoned cart + $row[Piwik_Archive::INDEX_ECOMMERCE_ORDERS] = $row[Piwik_Archive::INDEX_NB_VISITS]; + } + + unset($row[Piwik_Archive::INDEX_NB_VISITS]); + unset($row['label']); + unset($row['ecommerceType']); + + return $label; + } + + protected function roundColumnValues(&$row) + { + $columnsToRound = array( + Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE, + Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY, + Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE, + Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED, + ); + foreach ($columnsToRound as $column) { + if (isset($row[$column]) + && $row[$column] == round($row[$column]) + ) { + $row[$column] = round($row[$column]); + } + } + } + + protected function getEcommerceIdGoals() + { + return array(Piwik_Tracker_GoalManager::IDGOAL_CART, Piwik_Tracker_GoalManager::IDGOAL_ORDER); + } + + static public function getItemRecordNameAbandonedCart($recordName) + { + return $recordName . '_Cart'; } /** @@ -267,8 +376,8 @@ class Piwik_Goals_Archiver extends Piwik_PluginsArchiver * Archive Ecommerce Items */ if ($this->shouldArchiveEcommerceItems()) { - $dataTableToSum = $this->dimensions; - foreach ($this->dimensions as $recordName) { + $dataTableToSum = $this->dimensionRecord; + foreach ($this->dimensionRecord as $recordName) { $dataTableToSum[] = self::getItemRecordNameAbandonedCart($recordName); } $this->getProcessor()->archiveDataTable($dataTableToSum); @@ -303,51 +412,13 @@ class Piwik_Goals_Archiver extends Piwik_PluginsArchiver // sum up the visits to conversion data table & the days to conversion data table $this->getProcessor()->archiveDataTable(array( - self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $goalId), - self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $goalId))); + self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $goalId), + self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $goalId))); } // sum up goal overview reports $this->getProcessor()->archiveDataTable(array( - self::getRecordName(self::VISITS_UNTIL_RECORD_NAME), - self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME))); - } - - protected function shouldArchiveEcommerceItems() - { - // Per item doesn't support segment - // Also, when querying Goal metrics for visitorType==returning, we wouldnt want to trigger an extra request - // event if it did support segment - // (when this is implemented, we should have shouldProcessReportsForPlugin() support partial archiving based on which metric is requested) - if (!$this->getProcessor()->getSegment()->isEmpty()) { - return false; - } - return true; - } - - static public function getItemRecordNameAbandonedCart($recordName) - { - return $recordName . '_Cart'; + self::getRecordName(self::VISITS_UNTIL_RECORD_NAME), + self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME))); } - - /** - * @param string $recordName 'nb_conversions' - * @param int|bool $idGoal idGoal to return the metrics for, or false to return overall - * @return string Archive record name - */ - static public function getRecordName($recordName, $idGoal = false) - { - $idGoalStr = ''; - if ($idGoal !== false) { - $idGoalStr = $idGoal . "_"; - } - return 'Goal_' . $idGoalStr . $recordName; - } - - private function getConversionRate($count) - { - $visits = $this->getProcessor()->getNumberOfVisits(); - return round(100 * $count / $visits, Piwik_Tracker_GoalManager::REVENUE_PRECISION); - } - } \ No newline at end of file diff --git a/plugins/Provider/Archiver.php b/plugins/Provider/Archiver.php index 3cc703a0e2..692c3a72d2 100644 --- a/plugins/Provider/Archiver.php +++ b/plugins/Provider/Archiver.php @@ -11,19 +11,12 @@ class Piwik_Provider_Archiver extends Piwik_PluginsArchiver { const PROVIDER_RECORD_NAME = 'Provider_hostnameExt'; - protected $maximumRows; - - public function __construct($processor) - { - parent::__construct($processor); - $this->maximumRows = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; - } + const PROVIDER_FIELD = "location_provider"; public function archiveDay() { - $labelSQL = "log_visit.location_provider"; - $metricsByProvider = $this->getProcessor()->getMetricsForLabel($labelSQL); - $tableProvider = $this->getProcessor()->getDataTableFromArray($metricsByProvider); + $metrics = $this->getProcessor()->getMetricsForDimension(self::PROVIDER_FIELD); + $tableProvider = $this->getProcessor()->getDataTableFromDataArray($metrics); $this->getProcessor()->insertBlobRecord(self::PROVIDER_RECORD_NAME, $tableProvider->getSerialized($this->maximumRows, null, Piwik_Archive::INDEX_NB_VISITS)); } diff --git a/plugins/Referers/API.php b/plugins/Referers/API.php index abec869e56..e194b67df2 100644 --- a/plugins/Referers/API.php +++ b/plugins/Referers/API.php @@ -129,7 +129,7 @@ class Piwik_Referers_API public function getKeywords($idSite, $period, $date, $segment = false, $expanded = false) { - $dataTable = $this->getDataTable(Piwik_Referers_Archiver::SEARCH_ENGINE_BY_KEYWORD_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); + $dataTable = $this->getDataTable(Piwik_Referers_Archiver::KEYWORDS_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); $dataTable = $this->handleKeywordNotDefined($dataTable); return $dataTable; } @@ -212,7 +212,7 @@ class Piwik_Referers_API public function getSearchEnginesFromKeywordId($idSite, $period, $date, $idSubtable, $segment = false) { - $dataTable = $this->getDataTable(Piwik_Referers_Archiver::SEARCH_ENGINE_BY_KEYWORD_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); + $dataTable = $this->getDataTable(Piwik_Referers_Archiver::KEYWORDS_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_getSearchEngineUrlFromName')); $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik_getSearchEngineLogoFromUrl')); @@ -228,7 +228,7 @@ class Piwik_Referers_API public function getSearchEngines($idSite, $period, $date, $segment = false, $expanded = false) { - $dataTable = $this->getDataTable(Piwik_Referers_Archiver::KEYWORDS_BY_SEARCH_ENGINE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); + $dataTable = $this->getDataTable(Piwik_Referers_Archiver::SEARCH_ENGINES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', 'Piwik_getSearchEngineUrlFromName')); $dataTable->queueFilter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik_getSearchEngineLogoFromUrl')); return $dataTable; @@ -236,7 +236,7 @@ class Piwik_Referers_API public function getKeywordsFromSearchEngineId($idSite, $period, $date, $idSubtable, $segment = false) { - $dataTable = $this->getDataTable(Piwik_Referers_Archiver::KEYWORDS_BY_SEARCH_ENGINE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); + $dataTable = $this->getDataTable(Piwik_Referers_Archiver::SEARCH_ENGINES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); // get the search engine and create the URL to the search result page $searchEngines = $this->getSearchEngines($idSite, $period, $date, $segment); @@ -267,25 +267,25 @@ class Piwik_Referers_API public function getCampaigns($idSite, $period, $date, $segment = false, $expanded = false) { - $dataTable = $this->getDataTable(Piwik_Referers_Archiver::KEYWORD_BY_CAMPAIGN_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); + $dataTable = $this->getDataTable(Piwik_Referers_Archiver::CAMPAIGNS_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); return $dataTable; } public function getKeywordsFromCampaignId($idSite, $period, $date, $idSubtable, $segment = false) { - $dataTable = $this->getDataTable(Piwik_Referers_Archiver::KEYWORD_BY_CAMPAIGN_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); + $dataTable = $this->getDataTable(Piwik_Referers_Archiver::CAMPAIGNS_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); return $dataTable; } public function getWebsites($idSite, $period, $date, $segment = false, $expanded = false) { - $dataTable = $this->getDataTable(Piwik_Referers_Archiver::URL_BY_WEBSITE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); + $dataTable = $this->getDataTable(Piwik_Referers_Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); return $dataTable; } public function getUrlsFromWebsiteId($idSite, $period, $date, $idSubtable, $segment = false) { - $dataTable = $this->getDataTable(Piwik_Referers_Archiver::URL_BY_WEBSITE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); + $dataTable = $this->getDataTable(Piwik_Referers_Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = false, $idSubtable); // the htmlspecialchars_decode call is for BC for before 1.1 // as the Referer URL was previously encoded in the log tables, but is now recorded raw $dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'url', create_function('$label', 'return htmlspecialchars_decode($label);'))); @@ -308,7 +308,7 @@ class Piwik_Referers_API { require PIWIK_INCLUDE_PATH . '/core/DataFiles/Socials.php'; - $dataTable = $this->getDataTable( Piwik_Referers_Archiver::URL_BY_WEBSITE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); + $dataTable = $this->getDataTable( Piwik_Referers_Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded); $dataTable->filter('ColumnCallbackDeleteRow', array('label', 'Piwik_Referrers_isSocialUrl')); @@ -341,7 +341,7 @@ class Piwik_Referers_API { require PIWIK_INCLUDE_PATH . '/core/DataFiles/Socials.php'; - $dataTable = $this->getDataTable( Piwik_Referers_Archiver::URL_BY_WEBSITE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = true); + $dataTable = $this->getDataTable( Piwik_Referers_Archiver::WEBSITES_RECORD_NAME, $idSite, $period, $date, $segment, $expanded = true); // get the social network domain referred to by $idSubtable $social = false; diff --git a/plugins/Referers/Archiver.php b/plugins/Referers/Archiver.php index 58907702ad..ddae742131 100644 --- a/plugins/Referers/Archiver.php +++ b/plugins/Referers/Archiver.php @@ -11,30 +11,21 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver { - const KEYWORDS_BY_SEARCH_ENGINE_RECORD_NAME = 'Referers_keywordBySearchEngine'; - const SEARCH_ENGINE_BY_KEYWORD_RECORD_NAME = 'Referers_searchEngineByKeyword'; - const KEYWORD_BY_CAMPAIGN_RECORD_NAME = 'Referers_keywordByCampaign'; - const URL_BY_WEBSITE_RECORD_NAME = 'Referers_urlByWebsite'; + const SEARCH_ENGINES_RECORD_NAME = 'Referers_keywordBySearchEngine'; + const KEYWORDS_RECORD_NAME = 'Referers_searchEngineByKeyword'; + const CAMPAIGNS_RECORD_NAME = 'Referers_keywordByCampaign'; + const WEBSITES_RECORD_NAME = 'Referers_urlByWebsite'; const REFERER_TYPE_RECORD_NAME = 'Referers_type'; - const METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME = 'Referers_distinctSearchEngines'; const METRIC_DISTINCT_KEYWORD_RECORD_NAME = 'Referers_distinctKeywords'; const METRIC_DISTINCT_CAMPAIGN_RECORD_NAME = 'Referers_distinctCampaigns'; const METRIC_DISTINCT_WEBSITE_RECORD_NAME = 'Referers_distinctWebsites'; const METRIC_DISTINCT_URLS_RECORD_NAME = 'Referers_distinctWebsitesUrls'; - protected $columnToSortByBeforeTruncation; protected $maximumRowsInDataTableLevelZero; protected $maximumRowsInSubDataTable; - protected $metricsBySearchEngine = array(); - protected $metricsByKeyword = array(); - protected $metricsBySearchEngineAndKeyword = array(); - protected $metricsByKeywordAndSearchEngine = array(); - protected $metricsByWebsite = array(); - protected $metricsByWebsiteAndUrl = array(); - protected $metricsByCampaignAndKeyword = array(); - protected $metricsByCampaign = array(); - protected $metricsByType = array(); + /* @var array[Piwik_DataArray] $arrays */ + protected $arrays = array(); protected $distinctUrls = array(); function __construct($processor) @@ -47,6 +38,9 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver public function archiveDay() { + foreach ($this->getRecordNames() as $record) { + $this->arrays[$record] = new Piwik_DataArray(); + } $query = $this->getProcessor()->queryVisitsByDimension(array("referer_type", "referer_name", "referer_keyword", "referer_url")); $this->aggregateFromVisits($query); @@ -57,12 +51,22 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver $this->recordDayReports(); } + protected function getRecordNames() + { + return array( + self::REFERER_TYPE_RECORD_NAME, + self::KEYWORDS_RECORD_NAME, + self::SEARCH_ENGINES_RECORD_NAME, + self::WEBSITES_RECORD_NAME, + self::CAMPAIGNS_RECORD_NAME, + ); + } + protected function aggregateFromVisits($query) { while ($row = $query->fetch()) { $this->makeRefererTypeNonEmpty($row); $this->aggregateVisit($row); - $this->aggregateVisitByType($row); } } @@ -77,15 +81,32 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver { switch ($row['referer_type']) { case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - $this->aggregateVisitBySearchEngine($row); + if (empty($row['referer_keyword'])) { + $row['referer_keyword'] = Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED; + } + $searchEnginesArray = $this->getDataArray(self::SEARCH_ENGINES_RECORD_NAME); + $searchEnginesArray->sumMetricsVisits($row['referer_name'], $row); + $searchEnginesArray->sumMetricsVisitsPivot($row['referer_name'], $row['referer_keyword'], $row); + $keywordsDataArray = $this->getDataArray(self::KEYWORDS_RECORD_NAME); + $keywordsDataArray->sumMetricsVisits($row['referer_keyword'], $row); + $keywordsDataArray->sumMetricsVisitsPivot($row['referer_keyword'], $row['referer_name'], $row); break; case Piwik_Common::REFERER_TYPE_WEBSITE: - $this->aggregateVisitByWebsite($row); + $this->getDataArray(self::WEBSITES_RECORD_NAME)->sumMetricsVisits($row['referer_name'], $row); + $this->getDataArray(self::WEBSITES_RECORD_NAME)->sumMetricsVisitsPivot($row['referer_name'], $row['referer_url'], $row); + + $urlHash = substr(md5($row['referer_url']), 0, 10); + if (!isset($this->distinctUrls[$urlHash])) { + $this->distinctUrls[$urlHash] = true; + } break; case Piwik_Common::REFERER_TYPE_CAMPAIGN: - $this->aggregateVisitByCampaign($row); + if (!empty($row['referer_keyword'])) { + $this->getDataArray(self::CAMPAIGNS_RECORD_NAME)->sumMetricsVisitsPivot($row['referer_name'], $row['referer_keyword'], $row); + } + $this->getDataArray(self::CAMPAIGNS_RECORD_NAME)->sumMetricsVisits($row['referer_name'], $row); break; case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: @@ -96,70 +117,16 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver throw new Exception("Non expected referer_type = " . $row['referer_type']); break; } + $this->getDataArray(self::REFERER_TYPE_RECORD_NAME)->sumMetricsVisits($row['referer_type'], $row); } - protected function aggregateVisitBySearchEngine($row) - { - if (empty($row['referer_keyword'])) { - $row['referer_keyword'] = Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED; - } - if (!isset($this->metricsBySearchEngine[$row['referer_name']])) { - $this->metricsBySearchEngine[$row['referer_name']] = $this->getProcessor()->makeEmptyRow(); - } - if (!isset($this->metricsByKeyword[$row['referer_keyword']])) { - $this->metricsByKeyword[$row['referer_keyword']] = $this->getProcessor()->makeEmptyRow(); - } - if (!isset($this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']])) { - $this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']] = $this->getProcessor()->makeEmptyRow(); - } - if (!isset($this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']])) { - $this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']] = $this->getProcessor()->makeEmptyRow(); - } - - $this->getProcessor()->sumMetrics($row, $this->metricsBySearchEngine[$row['referer_name']]); - $this->getProcessor()->sumMetrics($row, $this->metricsByKeyword[$row['referer_keyword']]); - $this->getProcessor()->sumMetrics($row, $this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']]); - $this->getProcessor()->sumMetrics($row, $this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']]); - } - - protected function aggregateVisitByWebsite($row) - { - if (!isset($this->metricsByWebsite[$row['referer_name']])) { - $this->metricsByWebsite[$row['referer_name']] = $this->getProcessor()->makeEmptyRow(); - } - $this->getProcessor()->sumMetrics($row, $this->metricsByWebsite[$row['referer_name']]); - - if (!isset($this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']])) { - $this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']] = $this->getProcessor()->makeEmptyRow(); - } - $this->getProcessor()->sumMetrics($row, $this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']]); - - $urlHash = substr(md5($row['referer_url']), 0, 10); - if (!isset($this->distinctUrls[$urlHash])) { - $this->distinctUrls[$urlHash] = true; - } - } - - protected function aggregateVisitByCampaign($row) - { - if (!empty($row['referer_keyword'])) { - if (!isset($this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']])) { - $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']] = $this->getProcessor()->makeEmptyRow(); - } - $this->getProcessor()->sumMetrics($row, $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']]); - } - if (!isset($this->metricsByCampaign[$row['referer_name']])) { - $this->metricsByCampaign[$row['referer_name']] = $this->getProcessor()->makeEmptyRow(); - } - $this->getProcessor()->sumMetrics($row, $this->metricsByCampaign[$row['referer_name']]); - } - - protected function aggregateVisitByType($row) + /** + * @param $name + * @return Piwik_DataArray + */ + protected function getDataArray($name) { - if (!isset($this->metricsByType[$row['referer_type']])) { - $this->metricsByType[$row['referer_type']] = $this->getProcessor()->makeEmptyRow(); - } - $this->getProcessor()->sumMetrics($row, $this->metricsByType[$row['referer_type']]); + return $this->arrays[$name]; } protected function aggregateFromConversions($query) @@ -172,16 +139,14 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver $skipAggregateByType = $this->aggregateConversion($row); if (!$skipAggregateByType) { - $this->aggregateConversionByType($row); + $this->getDataArray(self::REFERER_TYPE_RECORD_NAME)->sumMetricsGoals($row['referer_type'], $row); } } - $this->getProcessor()->enrichMetricsWithConversions($this->metricsByType); - $this->getProcessor()->enrichMetricsWithConversions($this->metricsBySearchEngine); - $this->getProcessor()->enrichMetricsWithConversions($this->metricsByKeyword); - $this->getProcessor()->enrichMetricsWithConversions($this->metricsByWebsite); - $this->getProcessor()->enrichMetricsWithConversions($this->metricsByCampaign); - $this->getProcessor()->enrichPivotMetricsWithConversions($this->metricsByCampaignAndKeyword); + foreach ($this->arrays as $dataArray) { + /* @var Piwik_DataArray $dataArray */ + $dataArray->enrichMetricsWithConversions(); + } } protected function aggregateConversion($row) @@ -189,15 +154,23 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver $skipAggregateByType = false; switch ($row['referer_type']) { case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - $this->aggregateConversionBySearchEngine($row); + if (empty($row['referer_keyword'])) { + $row['referer_keyword'] = Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED; + } + + $this->getDataArray(self::SEARCH_ENGINES_RECORD_NAME)->sumMetricsGoals($row['referer_name'], $row); + $this->getDataArray(self::KEYWORDS_RECORD_NAME)->sumMetricsGoals($row['referer_keyword'], $row); break; case Piwik_Common::REFERER_TYPE_WEBSITE: - $this->aggregateConversionByWebsite($row); + $this->getDataArray(self::WEBSITES_RECORD_NAME)->sumMetricsGoals($row['referer_name'], $row); break; case Piwik_Common::REFERER_TYPE_CAMPAIGN: - $this->aggregateConversionByCampaign($row); + if (!empty($row['referer_keyword'])) { + $this->getDataArray(self::CAMPAIGNS_RECORD_NAME)->sumMetricsGoalsPivot($row['referer_name'], $row['referer_keyword'], $row); + } + $this->getDataArray(self::CAMPAIGNS_RECORD_NAME)->sumMetricsGoals($row['referer_name'], $row); break; case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY: @@ -213,52 +186,6 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver return $skipAggregateByType; } - protected function aggregateConversionBySearchEngine($row) - { - if (empty($row['referer_keyword'])) { - $row['referer_keyword'] = Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED; - } - if (!isset($this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) { - $this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $this->getProcessor()->makeEmptyGoalRow($row['idgoal']); - } - if (!isset($this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) { - $this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $this->getProcessor()->makeEmptyGoalRow($row['idgoal']); - } - - $this->getProcessor()->sumGoalMetrics($row, $this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - $this->getProcessor()->sumGoalMetrics($row, $this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - } - - protected function aggregateConversionByWebsite($row) - { - if (!isset($this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) { - $this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $this->getProcessor()->makeEmptyGoalRow($row['idgoal']); - } - $this->getProcessor()->sumGoalMetrics($row, $this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - } - - protected function aggregateConversionByCampaign($row) - { - if (!empty($row['referer_keyword'])) { - if (!isset($this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) { - $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $this->getProcessor()->makeEmptyGoalRow($row['idgoal']); - } - $this->getProcessor()->sumGoalMetrics($row, $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - } - if (!isset($this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) { - $this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $this->getProcessor()->makeEmptyGoalRow($row['idgoal']); - } - $this->getProcessor()->sumGoalMetrics($row, $this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - } - - protected function aggregateConversionByType($row) - { - if (!isset($this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) { - $this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $this->getProcessor()->makeEmptyGoalRow($row['idgoal']); - } - $this->getProcessor()->sumGoalMetrics($row, $this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); - } - /** * Records the daily stats (numeric or datatable blob) into the archive tables. * @@ -273,31 +200,21 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver protected function recordDayNumeric() { $numericRecords = array( - self::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME => count($this->metricsBySearchEngineAndKeyword), - self::METRIC_DISTINCT_KEYWORD_RECORD_NAME => count($this->metricsByKeywordAndSearchEngine), - self::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME => count($this->metricsByCampaign), - self::METRIC_DISTINCT_WEBSITE_RECORD_NAME => count($this->metricsByWebsite), + self::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME => count($this->getDataArray(self::SEARCH_ENGINES_RECORD_NAME)), + self::METRIC_DISTINCT_KEYWORD_RECORD_NAME => count($this->getDataArray(self::KEYWORDS_RECORD_NAME)), + self::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME => count($this->getDataArray(self::CAMPAIGNS_RECORD_NAME)), + self::METRIC_DISTINCT_WEBSITE_RECORD_NAME => count($this->getDataArray(self::WEBSITES_RECORD_NAME)), self::METRIC_DISTINCT_URLS_RECORD_NAME => count($this->distinctUrls), ); - foreach ($numericRecords as $name => $value) { - $this->getProcessor()->insertNumericRecord($name, $value); - } + $this->getProcessor()->insertNumericRecords($numericRecords); } protected function recordDayBlobs() { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($this->metricsByType); - $this->getProcessor()->insertBlobRecord(self::REFERER_TYPE_RECORD_NAME, $table->getSerialized()); - - $blobRecords = array( - self::KEYWORDS_BY_SEARCH_ENGINE_RECORD_NAME => $this->getProcessor()->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsBySearchEngineAndKeyword, $this->metricsBySearchEngine), - self::SEARCH_ENGINE_BY_KEYWORD_RECORD_NAME => $this->getProcessor()->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByKeywordAndSearchEngine, $this->metricsByKeyword), - self::KEYWORD_BY_CAMPAIGN_RECORD_NAME => $this->getProcessor()->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByCampaignAndKeyword, $this->metricsByCampaign), - self::URL_BY_WEBSITE_RECORD_NAME => $this->getProcessor()->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByWebsiteAndUrl, $this->metricsByWebsite), - ); - foreach ($blobRecords as $recordName => $table) { + foreach ($this->getRecordNames() as $recordName) { + $dataArray = $this->getDataArray($recordName); + $table = $this->getProcessor()->getDataTableFromDataArray($dataArray); $blob = $table->getSerialized($this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation); $this->getProcessor()->insertBlobRecord($recordName, $blob); } @@ -305,35 +222,29 @@ class Piwik_Referers_Archiver extends Piwik_PluginsArchiver public function archivePeriod() { - $dataTableToSum = array( - self::REFERER_TYPE_RECORD_NAME, - self::KEYWORDS_BY_SEARCH_ENGINE_RECORD_NAME, - self::SEARCH_ENGINE_BY_KEYWORD_RECORD_NAME, - self::KEYWORD_BY_CAMPAIGN_RECORD_NAME, - self::URL_BY_WEBSITE_RECORD_NAME, - ); + $dataTableToSum = $this->getRecordNames(); $nameToCount = $this->getProcessor()->archiveDataTable($dataTableToSum, null, $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation); $mappingFromArchiveName = array( self::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME => array('typeCountToUse' => 'level0', - 'nameTableToUse' => self::KEYWORDS_BY_SEARCH_ENGINE_RECORD_NAME, + 'nameTableToUse' => self::SEARCH_ENGINES_RECORD_NAME, ), self::METRIC_DISTINCT_KEYWORD_RECORD_NAME => array('typeCountToUse' => 'level0', - 'nameTableToUse' => self::SEARCH_ENGINE_BY_KEYWORD_RECORD_NAME, + 'nameTableToUse' => self::KEYWORDS_RECORD_NAME, ), self::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME => array('typeCountToUse' => 'level0', - 'nameTableToUse' => self::KEYWORD_BY_CAMPAIGN_RECORD_NAME, + 'nameTableToUse' => self::CAMPAIGNS_RECORD_NAME, ), self::METRIC_DISTINCT_WEBSITE_RECORD_NAME => array('typeCountToUse' => 'level0', - 'nameTableToUse' => self::URL_BY_WEBSITE_RECORD_NAME, + 'nameTableToUse' => self::WEBSITES_RECORD_NAME, ), self::METRIC_DISTINCT_URLS_RECORD_NAME => array('typeCountToUse' => 'recursive', - 'nameTableToUse' => self::URL_BY_WEBSITE_RECORD_NAME, + 'nameTableToUse' => self::WEBSITES_RECORD_NAME, ), ); diff --git a/plugins/SEO/API.php b/plugins/SEO/API.php index 26ac00c5bd..bc94a2663d 100644 --- a/plugins/SEO/API.php +++ b/plugins/SEO/API.php @@ -100,8 +100,6 @@ class Piwik_SEO_API $data[Piwik_Translate('SEO_Dmoz')] = $dmozRank; } - $dataTable = new Piwik_DataTable(); - $dataTable->addRowsFromArrayWithIndexLabel($data); - return $dataTable; + return Piwik_DataTable::makeFromIndexedArray($data); } } diff --git a/plugins/Transitions/Transitions.php b/plugins/Transitions/Transitions.php index 765854bff7..b92ef87b8c 100644 --- a/plugins/Transitions/Transitions.php +++ b/plugins/Transitions/Transitions.php @@ -79,16 +79,15 @@ class Piwik_Transitions extends Piwik_Plugin // group by. when we group by both, we don't get a single column for the keyword but instead // one column per keyword + search engine url. this way, we could not get the top keywords using // the ranking query. - $dimension = 'referrer_data'; + $dimensions = array('referrer_data', 'referer_type'); $rankingQuery->addLabelColumn('referrer_data'); - $select = ' - CASE referer_type + $selects = array( + 'CASE referer_type WHEN ' . Piwik_Common::REFERER_TYPE_DIRECT_ENTRY . ' THEN \'\' WHEN ' . Piwik_Common::REFERER_TYPE_SEARCH_ENGINE . ' THEN referer_keyword WHEN ' . Piwik_Common::REFERER_TYPE_WEBSITE . ' THEN referer_url WHEN ' . Piwik_Common::REFERER_TYPE_CAMPAIGN . ' THEN CONCAT(referer_name, \' \', referer_keyword) - END AS referrer_data, - referer_type'; + END AS referrer_data'); // get one limited group per referrer type $rankingQuery->partitionResultIntoMultipleGroups('referer_type', array( @@ -98,14 +97,11 @@ class Piwik_Transitions extends Piwik_Plugin Piwik_Common::REFERER_TYPE_CAMPAIGN )); - $orderBy = '`' . Piwik_Archive::INDEX_NB_VISITS . '` DESC'; - $type = $this->getColumnTypeSuffix($actionType); $where = 'visit_entry_idaction_' . $type . ' = ' . intval($idaction); $metrics = array(Piwik_Archive::INDEX_NB_VISITS); - $data = $archiveProcessing->queryVisitsByDimension($dimension, $where, $metrics, $orderBy, - $rankingQuery, $select, $selectGeneratesLabelColumn = true); + $data = $archiveProcessing->queryVisitsByDimension($dimensions, $where, $selects, $metrics, $rankingQuery); $referrerData = array(); $referrerSubData = array(); @@ -132,7 +128,9 @@ class Piwik_Transitions extends Piwik_Plugin } } - return $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($referrerSubData, $referrerData); + //FIXMEA refactor after integration tests written + $array = new Piwik_DataArray($referrerData, $referrerSubData); + return Piwik_ArchiveProcessing_Day::getDataTableFromDataArray($array); } /** @@ -161,14 +159,16 @@ class Piwik_Transitions extends Piwik_Plugin $dimension = 'idaction_name_ref'; } - $addSelect = ' - log_action.name, log_action.url_prefix, - CASE WHEN log_link_visit_action.idaction_' . $type . '_ref = ' . intval($idaction) . ' THEN 1 ELSE 0 END AS is_self, - CASE - WHEN log_action.type = ' . $mainActionType . ' THEN 1 - WHEN log_action.type = ' . Piwik_Tracker_Action::TYPE_SITE_SEARCH . ' THEN 2 - ELSE 0 - END AS action_partition'; + $selects = array( + 'log_action.name', + 'log_action.url_prefix', + 'CASE WHEN log_link_visit_action.idaction_' . $type . '_ref = ' . intval($idaction) . ' THEN 1 ELSE 0 END AS is_self', + 'CASE + WHEN log_action.type = ' . $mainActionType . ' THEN 1 + WHEN log_action.type = ' . Piwik_Tracker_Action::TYPE_SITE_SEARCH . ' THEN 2 + ELSE 0 + END AS action_partition' + ); $where = ' log_link_visit_action.idaction_' . $type . ' = ' . intval($idaction); @@ -183,11 +183,8 @@ class Piwik_Transitions extends Piwik_Plugin $dimension = array($dimension); } - $orderBy = '`' . Piwik_Archive::INDEX_NB_ACTIONS . '` DESC'; - $metrics = array(Piwik_Archive::INDEX_NB_ACTIONS); - $data = $archiveProcessing->queryActionsByDimension($dimension, $where, $metrics, $orderBy, - $rankingQuery, $joinLogActionOn, $addSelect); + $data = $archiveProcessing->queryActionsByDimension($dimension, $where, $selects, $metrics, $rankingQuery, $joinLogActionOn); $loops = 0; $nbPageviews = 0; @@ -278,7 +275,7 @@ class Piwik_Transitions extends Piwik_Plugin // site search referrers are logged with url=NULL // when we find one, we have to join on name $joinLogActionColumn = $dimension; - $addSelect = 'log_action.name, log_action.url_prefix, log_action.type'; + $selects = array('log_action.name', 'log_action.url_prefix', 'log_action.type'); } else { // specific setup for page titles: $types[Piwik_Tracker_Action::TYPE_ACTION_NAME] = 'followingPages'; @@ -295,25 +292,25 @@ class Piwik_Transitions extends Piwik_Plugin ELSE log_action1.idaction END '; - $addSelect = ' - CASE + $selects = array( + 'CASE ' /* following site search */ . ' WHEN log_link_visit_action.idaction_url IS NULL THEN log_action2.name ' /* following page view: use page title */ . ' WHEN log_action1.type = ' . Piwik_Tracker_Action::TYPE_ACTION_URL . ' THEN log_action2.name ' /* following download or outlink: use url */ . ' ELSE log_action1.name - END AS name, - CASE + END AS name', + 'CASE ' /* following site search */ . ' WHEN log_link_visit_action.idaction_url IS NULL THEN log_action2.type ' /* following page view: use page title */ . ' WHEN log_action1.type = ' . Piwik_Tracker_Action::TYPE_ACTION_URL . ' THEN log_action2.type ' /* following download or outlink: use url */ . ' ELSE log_action1.type - END AS type, - NULL AS url_prefix - '; + END AS type', + 'NULL AS url_prefix' + ); } // these types are available for both titles and urls @@ -332,11 +329,8 @@ class Piwik_Transitions extends Piwik_Plugin . 'log_link_visit_action.idaction_' . $type . ' != ' . intval($idaction) . ')'; } - $orderBy = '`' . Piwik_Archive::INDEX_NB_ACTIONS . '` DESC'; - $metrics = array(Piwik_Archive::INDEX_NB_ACTIONS); - $data = $archiveProcessing->queryActionsByDimension($dimension, $where, $metrics, $orderBy, - $rankingQuery, $joinLogActionColumn, $addSelect); + $data = $archiveProcessing->queryActionsByDimension($dimension, $where, $selects, $metrics, $rankingQuery, $joinLogActionColumn); $this->totalTransitionsToFollowingActions = 0; $dataTables = array(); diff --git a/plugins/UserCountry/API.php b/plugins/UserCountry/API.php index 15f5f69ecf..40432969e9 100644 --- a/plugins/UserCountry/API.php +++ b/plugins/UserCountry/API.php @@ -32,7 +32,7 @@ class Piwik_UserCountry_API public function getCountry($idSite, $period, $date, $segment = false) { - $dataTable = $this->getDataTable(Piwik_UserCountry_Archiver::VISITS_BY_COUNTRY_RECORD_NAME, $idSite, $period, $date, $segment); + $dataTable = $this->getDataTable(Piwik_UserCountry_Archiver::COUNTRY_RECORD_NAME, $idSite, $period, $date, $segment); // apply filter on the whole datatable in order the inline search to work (searches are done on "beautiful" label) $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'code')); @@ -46,7 +46,7 @@ class Piwik_UserCountry_API public function getContinent($idSite, $period, $date, $segment = false) { - $dataTable = $this->getDataTable(Piwik_UserCountry_Archiver::VISITS_BY_COUNTRY_RECORD_NAME, $idSite, $period, $date, $segment); + $dataTable = $this->getDataTable(Piwik_UserCountry_Archiver::COUNTRY_RECORD_NAME, $idSite, $period, $date, $segment); $getContinent = array('Piwik_Common', 'getContinent'); $dataTable->filter('GroupBy', array('label', $getContinent)); @@ -68,7 +68,7 @@ class Piwik_UserCountry_API */ public function getRegion($idSite, $period, $date, $segment = false) { - $dataTable = $this->getDataTable(Piwik_UserCountry_Archiver::VISITS_BY_REGION_RECORD_NAME, $idSite, $period, $date, $segment); + $dataTable = $this->getDataTable(Piwik_UserCountry_Archiver::REGION_RECORD_NAME, $idSite, $period, $date, $segment); $separator = Piwik_UserCountry_Archiver::LOCATION_SEPARATOR; $unk = Piwik_Tracker_Visit::UNKNOWN_CODE; @@ -110,7 +110,7 @@ class Piwik_UserCountry_API */ public function getCity($idSite, $period, $date, $segment = false) { - $dataTable = $this->getDataTable(Piwik_UserCountry_Archiver::VISITS_BY_CITY_RECORD_NAME, $idSite, $period, $date, $segment); + $dataTable = $this->getDataTable(Piwik_UserCountry_Archiver::CITY_RECORD_NAME, $idSite, $period, $date, $segment); $separator = Piwik_UserCountry_Archiver::LOCATION_SEPARATOR; $unk = Piwik_Tracker_Visit::UNKNOWN_CODE; diff --git a/plugins/UserCountry/Archiver.php b/plugins/UserCountry/Archiver.php index ecd124d67f..fb69e89ff8 100644 --- a/plugins/UserCountry/Archiver.php +++ b/plugins/UserCountry/Archiver.php @@ -11,9 +11,9 @@ class Piwik_UserCountry_Archiver extends Piwik_PluginsArchiver { - const VISITS_BY_COUNTRY_RECORD_NAME = 'UserCountry_country'; - const VISITS_BY_REGION_RECORD_NAME = 'UserCountry_region'; - const VISITS_BY_CITY_RECORD_NAME = 'UserCountry_city'; + const COUNTRY_RECORD_NAME = 'UserCountry_country'; + const REGION_RECORD_NAME = 'UserCountry_region'; + const CITY_RECORD_NAME = 'UserCountry_city'; const DISTINCT_COUNTRIES_METRIC = 'UserCountry_distinctCountries'; // separate region, city & country info in stored report labels @@ -21,15 +21,28 @@ class Piwik_UserCountry_Archiver extends Piwik_PluginsArchiver private $latLongForCities = array(); - private $metricsByDimension = array(); + private $dataArrays = array(); protected $maximumRows; + const COUNTRY_FIELD = 'location_country'; + + const REGION_FIELD = 'location_region'; + + const CITY_FIELD = 'location_city'; + + protected $dimensions = array( self::COUNTRY_FIELD, self::REGION_FIELD, self::CITY_FIELD ); + + protected $arrays; + const LATITUDE_FIELD = 'location_latitude'; + const LONGITUDE_FIELD = 'location_longitude'; + + public function archiveDay() { - $this->metricsByDimension = array('location_country' => array(), - 'location_region' => array(), - 'location_city' => array()); + foreach($this->dimensions as $dimension) { + $this->arrays[$dimension] = new Piwik_DataArray(); + } $this->aggregateFromVisits(); $this->aggregateFromConversions(); $this->recordDayReports(); @@ -37,17 +50,9 @@ class Piwik_UserCountry_Archiver extends Piwik_PluginsArchiver protected function aggregateFromVisits() { - $dimensions = array_keys($this->metricsByDimension); - $query = $this->getProcessor()->queryVisitsByDimension( - $dimensions, - $where = '', - $metrics = false, - $orderBy = false, - $rankingQuery = null, - $addSelect = 'MAX(log_visit.location_latitude) as location_latitude, - MAX(log_visit.location_longitude) as location_longitude' - ); - + $additionalSelects = array('MAX(log_visit.location_latitude) as location_latitude', + 'MAX(log_visit.location_longitude) as location_longitude'); + $query = $this->getProcessor()->queryVisitsByDimension($this->dimensions, $where = false, $additionalSelects); if ($query === false) { return; } @@ -55,7 +60,11 @@ class Piwik_UserCountry_Archiver extends Piwik_PluginsArchiver while ($row = $query->fetch()) { $this->makeRegionCityLabelsUnique($row); $this->rememberCityLatLong($row); - $this->aggregateVisit($row); + + /* @var $dataArray Piwik_DataArray */ + foreach ($this->arrays as $dimension => $dataArray) { + $dataArray->sumMetricsVisits($row[$dimension], $row); + } } } @@ -66,61 +75,33 @@ class Piwik_UserCountry_Archiver extends Piwik_PluginsArchiver */ private function makeRegionCityLabelsUnique(&$row) { - static $locationColumns = array('location_region', 'location_country', 'location_city'); - - // to be on the safe side, remove the location separator from the region/city/country we - // get from the query - foreach ($locationColumns as $column) { + // remove the location separator from the region/city/country we get from the query + foreach ($this->dimensions as $column) { $row[$column] = str_replace(self::LOCATION_SEPARATOR, '', $row[$column]); } - if (!empty($row['location_region'])) // do not differentiate between unknown regions - { - $row['location_region'] = $row['location_region'] . self::LOCATION_SEPARATOR . $row['location_country']; + if (!empty($row[self::REGION_FIELD])) { + $row[self::REGION_FIELD] = $row[self::REGION_FIELD] . self::LOCATION_SEPARATOR . $row[self::COUNTRY_FIELD]; } - if (!empty($row['location_city'])) // do not differentiate between unknown cities - { - $row['location_city'] = $row['location_city'] . self::LOCATION_SEPARATOR . $row['location_region']; + if (!empty($row[self::CITY_FIELD])) { + $row[self::CITY_FIELD] = $row[self::CITY_FIELD] . self::LOCATION_SEPARATOR . $row[self::REGION_FIELD]; } } protected function rememberCityLatLong($row) { - $lat = $long = false; - if (!empty($row['location_city'])) { - if (!empty($row['location_latitude'])) { - $lat = $row['location_latitude']; - } - if (!empty($row['location_longitude'])) { - $long = $row['location_longitude']; - } - } - - // store latitude/longitude, if we should - if ($lat !== false && $long !== false - && empty($this->latLongForCities[$row['location_city']]) - ) { - $this->latLongForCities[$row['location_city']] = array($lat, $long); - } - } - - protected function aggregateVisit($row) - { - foreach ($this->metricsByDimension as $dimension => &$table) { - $label = (string)$row[$dimension]; - - if (!isset($table[$label])) { - $table[$label] = $this->getProcessor()->makeEmptyRow(); - } - $this->getProcessor()->sumMetrics($row, $table[$label]); + if ( !empty($row[self::CITY_FIELD]) + && !empty($row[self::LATITUDE_FIELD]) + && !empty($row[self::LONGITUDE_FIELD]) + && empty($this->latLongForCities[$row[self::CITY_FIELD]])) { + $this->latLongForCities[$row[self::CITY_FIELD]] = array($row[self::LATITUDE_FIELD], $row[self::LONGITUDE_FIELD]); } } protected function aggregateFromConversions() { - $dimensions = array_keys($this->metricsByDimension); - $query = $this->getProcessor()->queryConversionsByDimension($dimensions); + $query = $this->getProcessor()->queryConversionsByDimension($this->dimensions); if ($query === false) { return; @@ -129,36 +110,32 @@ class Piwik_UserCountry_Archiver extends Piwik_PluginsArchiver while ($row = $query->fetch()) { $this->makeRegionCityLabelsUnique($row); - $idGoal = $row['idgoal']; - foreach ($this->metricsByDimension as $dimension => &$table) { - $label = (string)$row[$dimension]; - - if (!isset($table[$label][Piwik_Archive::INDEX_GOALS][$idGoal])) { - $table[$label][Piwik_Archive::INDEX_GOALS][$idGoal] = $this->getProcessor()->makeEmptyGoalRow($idGoal); - } - $this->getProcessor()->sumGoalMetrics($row, $table[$label][Piwik_Archive::INDEX_GOALS][$idGoal]); + /* @var $dataArray Piwik_DataArray */ + foreach ($this->arrays as $dimension => $dataArray) { + $dataArray->sumMetricsGoals($row[$dimension], $row); } } - foreach ($this->metricsByDimension as &$table) { - $this->getProcessor()->enrichMetricsWithConversions($table); + /* @var $dataArray Piwik_DataArray */ + foreach ($this->arrays as $dataArray) { + $dataArray->enrichMetricsWithConversions(); } } protected function recordDayReports() { - $tableCountry = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->metricsByDimension['location_country']); - $this->getProcessor()->insertBlobRecord(self::VISITS_BY_COUNTRY_RECORD_NAME, $tableCountry->getSerialized()); + $tableCountry = Piwik_ArchiveProcessing_Day::getDataTableFromDataArray($this->arrays[self::COUNTRY_FIELD]); + $this->getProcessor()->insertBlobRecord(self::COUNTRY_RECORD_NAME, $tableCountry->getSerialized()); $this->getProcessor()->insertNumericRecord(self::DISTINCT_COUNTRIES_METRIC, $tableCountry->getRowsCount()); - $tableRegion = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->metricsByDimension['location_region']); + $tableRegion = Piwik_ArchiveProcessing_Day::getDataTableFromDataArray($this->arrays[self::REGION_FIELD]); $serialized = $tableRegion->getSerialized($this->maximumRows, $this->maximumRows, Piwik_Archive::INDEX_NB_VISITS); - $this->getProcessor()->insertBlobRecord(self::VISITS_BY_REGION_RECORD_NAME, $serialized); + $this->getProcessor()->insertBlobRecord(self::REGION_RECORD_NAME, $serialized); - $tableCity = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->metricsByDimension['location_city']); + $tableCity = Piwik_ArchiveProcessing_Day::getDataTableFromDataArray($this->arrays[self::CITY_FIELD]); $this->setLatitudeLongitude($tableCity); $serialized = $tableCity->getSerialized($this->maximumRows, $this->maximumRows, Piwik_Archive::INDEX_NB_VISITS); - $this->getProcessor()->insertBlobRecord(self::VISITS_BY_CITY_RECORD_NAME, $serialized); + $this->getProcessor()->insertBlobRecord(self::CITY_RECORD_NAME, $serialized); } /** @@ -185,14 +162,14 @@ class Piwik_UserCountry_Archiver extends Piwik_PluginsArchiver public function archivePeriod() { $dataTableToSum = array( - self::VISITS_BY_COUNTRY_RECORD_NAME, - self::VISITS_BY_REGION_RECORD_NAME, - self::VISITS_BY_CITY_RECORD_NAME, + self::COUNTRY_RECORD_NAME, + self::REGION_RECORD_NAME, + self::CITY_RECORD_NAME, ); $nameToCount = $this->getProcessor()->archiveDataTable($dataTableToSum); $this->getProcessor()->insertNumericRecord(self::DISTINCT_COUNTRIES_METRIC, - $nameToCount[self::VISITS_BY_COUNTRY_RECORD_NAME]['level0']); + $nameToCount[self::COUNTRY_RECORD_NAME]['level0']); } } \ No newline at end of file diff --git a/plugins/UserSettings/API.php b/plugins/UserSettings/API.php index 4369d397b9..03ea9ac61d 100644 --- a/plugins/UserSettings/API.php +++ b/plugins/UserSettings/API.php @@ -189,7 +189,6 @@ class Piwik_UserSettings_API // walk through the results and calculate the percentage foreach ($tableArray as $key => $table) { - // get according browserType table foreach ($browserTypesArray AS $k => $browsers) { if ($k == $key) { diff --git a/plugins/UserSettings/Archiver.php b/plugins/UserSettings/Archiver.php index 7b43657eeb..a27db178ca 100644 --- a/plugins/UserSettings/Archiver.php +++ b/plugins/UserSettings/Archiver.php @@ -22,12 +22,11 @@ class Piwik_UserSettings_Archiver extends Piwik_PluginsArchiver const OS_RECORD_NAME = 'UserSettings_os'; const CONFIGURATION_RECORD_NAME = 'UserSettings_configuration'; - public function __construct($processor) - { - parent::__construct($processor); - $this->maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard']; - $this->columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS; - } + const LANGUAGE_DIMENSION = "log_visit.location_browser_lang"; + const RESOLUTION_DIMENSION = "log_visit.config_resolution"; + const BROWSER_VERSION_DIMENSION = "CONCAT(log_visit.config_browser_name, ';', log_visit.config_browser_version)"; + const OS_DIMENSION = "log_visit.config_os"; + const CONFIGURATION_DIMENSION = "CONCAT(log_visit.config_os, ';', log_visit.config_browser_name, ';', log_visit.config_resolution)"; public function archiveDay() { @@ -41,17 +40,16 @@ class Piwik_UserSettings_Archiver extends Piwik_PluginsArchiver protected function aggregateByConfiguration() { - $labelSQL = "CONCAT(log_visit.config_os, ';', log_visit.config_browser_name, ';', log_visit.config_resolution)"; - $metrics = $this->getProcessor()->getMetricsForLabel($labelSQL); - $table = $this->getProcessor()->getDataTableFromArray($metrics); - $this->getProcessor()->insertBlobRecord(self::CONFIGURATION_RECORD_NAME, $table->getSerialized($this->maximumRowsInDataTable, null, $this->columnToSortByBeforeTruncation)); + $metrics = $this->getProcessor()->getMetricsForDimension(self::CONFIGURATION_DIMENSION); + $table = $this->getProcessor()->getDataTableFromDataArray($metrics); + $this->insertTable(self::CONFIGURATION_RECORD_NAME, $table); } protected function aggregateByOs() { - $metrics = $this->getProcessor()->getMetricsForLabel("log_visit.config_os"); - $table = $this->getProcessor()->getDataTableFromArray($metrics); - $this->getProcessor()->insertBlobRecord(self::OS_RECORD_NAME, $table->getSerialized($this->maximumRowsInDataTable, null, $this->columnToSortByBeforeTruncation)); + $metrics = $this->getProcessor()->getMetricsForDimension(self::OS_DIMENSION); + $table = $this->getProcessor()->getDataTableFromDataArray($metrics); + $this->insertTable(self::OS_RECORD_NAME, $table); } protected function aggregateByBrowser() @@ -62,18 +60,16 @@ class Piwik_UserSettings_Archiver extends Piwik_PluginsArchiver protected function aggregateByBrowserVersion() { - $labelSQL = "CONCAT(log_visit.config_browser_name, ';', log_visit.config_browser_version)"; - $metrics = $this->getProcessor()->getMetricsForLabel($labelSQL); - $tableBrowser = $this->getProcessor()->getDataTableFromArray($metrics); - - $this->getProcessor()->insertBlobRecord(self::BROWSER_RECORD_NAME, $tableBrowser->getSerialized($this->maximumRowsInDataTable, null, $this->columnToSortByBeforeTruncation)); + $metrics = $this->getProcessor()->getMetricsForDimension(self::BROWSER_VERSION_DIMENSION); + $tableBrowser = $this->getProcessor()->getDataTableFromDataArray($metrics); + $this->insertTable(self::BROWSER_RECORD_NAME, $tableBrowser); return $tableBrowser; } protected function aggregateByBrowserType(Piwik_DataTable $tableBrowser) { $tableBrowser->filter('GroupBy', array('label', 'Piwik_getBrowserFamily')); - $this->getProcessor()->insertBlobRecord(self::BROWSER_TYPE_RECORD_NAME, $tableBrowser->getSerialized()); + $this->insertTable(self::BROWSER_TYPE_RECORD_NAME, $tableBrowser); } protected function aggregateByResolutionAndScreenType() @@ -84,53 +80,58 @@ class Piwik_UserSettings_Archiver extends Piwik_PluginsArchiver protected function aggregateByResolution() { - $metrics = $this->getProcessor()->getMetricsForLabel("log_visit.config_resolution"); - $table = $this->getProcessor()->getDataTableFromArray($metrics); + $metrics = $this->getProcessor()->getMetricsForDimension(self::RESOLUTION_DIMENSION); + $table = $this->getProcessor()->getDataTableFromDataArray($metrics); $table->filter('ColumnCallbackDeleteRow', array('label', 'Piwik_UserSettings_keepStrlenGreater')); - $this->getProcessor()->insertBlobRecord(self::RESOLUTION_RECORD_NAME, $table->getSerialized($this->maximumRowsInDataTable, null, $this->columnToSortByBeforeTruncation)); + $this->insertTable(self::RESOLUTION_RECORD_NAME, $table); return $table; } protected function aggregateByScreenType(Piwik_DataTable $resolutions) { $resolutions->filter('GroupBy', array('label', 'Piwik_getScreenTypeFromResolution')); - $this->getProcessor()->insertBlobRecord(self::SCREEN_TYPE_RECORD_NAME, $resolutions->getSerialized()); + $this->insertTable(self::SCREEN_TYPE_RECORD_NAME, $resolutions); } protected function aggregateByPlugin() { - $toSelect = "sum(case log_visit.config_pdf when 1 then 1 else 0 end) as pdf, - sum(case log_visit.config_flash when 1 then 1 else 0 end) as flash, - sum(case log_visit.config_java when 1 then 1 else 0 end) as java, - sum(case log_visit.config_director when 1 then 1 else 0 end) as director, - sum(case log_visit.config_quicktime when 1 then 1 else 0 end) as quicktime, - sum(case log_visit.config_realplayer when 1 then 1 else 0 end) as realplayer, - sum(case log_visit.config_windowsmedia when 1 then 1 else 0 end) as windowsmedia, - sum(case log_visit.config_gears when 1 then 1 else 0 end) as gears, - sum(case log_visit.config_silverlight when 1 then 1 else 0 end) as silverlight, - sum(case log_visit.config_cookie when 1 then 1 else 0 end) as cookie "; - - $data = $this->getProcessor()->queryVisitsSimple($toSelect); - $table = $this->getProcessor()->getSimpleDataTableFromRow($data, Piwik_Archive::INDEX_NB_VISITS); - $this->getProcessor()->insertBlobRecord(self::PLUGIN_RECORD_NAME, $table->getSerialized()); + $selects = array( + "sum(case log_visit.config_pdf when 1 then 1 else 0 end) as pdf", + "sum(case log_visit.config_flash when 1 then 1 else 0 end) as flash", + "sum(case log_visit.config_java when 1 then 1 else 0 end) as java", + "sum(case log_visit.config_director when 1 then 1 else 0 end) as director", + "sum(case log_visit.config_quicktime when 1 then 1 else 0 end) as quicktime", + "sum(case log_visit.config_realplayer when 1 then 1 else 0 end) as realplayer", + "sum(case log_visit.config_windowsmedia when 1 then 1 else 0 end) as windowsmedia", + "sum(case log_visit.config_gears when 1 then 1 else 0 end) as gears", + "sum(case log_visit.config_silverlight when 1 then 1 else 0 end) as silverlight", + "sum(case log_visit.config_cookie when 1 then 1 else 0 end) as cookie" + ); + + $query = $this->getProcessor()->queryVisitsByDimension(array(), false, $selects, $metrics = array()); + $data = $query->fetch(); + $cleanRow = Piwik_DataAccess_LogAggregator::makeArrayOneColumn($data, Piwik_Archive::INDEX_NB_VISITS); + $table = Piwik_DataTable::makeFromIndexedArray($cleanRow); + $this->insertTable(self::PLUGIN_RECORD_NAME, $table); } protected function aggregateByLanguage() { - $query = $this->getProcessor()->queryVisitsByDimension("log_visit.location_browser_lang"); + $query = $this->getProcessor()->queryVisitsByDimension( array("label" => self::LANGUAGE_DIMENSION) ); $languageCodes = array_keys(Piwik_Common::getLanguagesList()); - $metricsByLanguage = array(); + $metricsByLanguage = new Piwik_DataArray(); while ($row = $query->fetch()) { $code = Piwik_Common::extractLanguageCodeFromBrowserLanguage($row['label'], $languageCodes); - - if (!isset($metricsByLanguage[$code])) { - $metricsByLanguage[$code] = $this->getProcessor()->makeEmptyRow(); - } - $this->getProcessor()->sumMetrics($row, $metricsByLanguage[$code]); + $metricsByLanguage->sumMetricsVisits($code, $row); } - $tableLanguage = $this->getProcessor()->getDataTableFromArray($metricsByLanguage); - $this->getProcessor()->insertBlobRecord(self::LANGUAGE_RECORD_NAME, $tableLanguage->getSerialized($this->maximumRowsInDataTable, null, $this->columnToSortByBeforeTruncation)); + $tableLanguage = $this->getProcessor()->getDataTableFromDataArray($metricsByLanguage); + $this->insertTable(self::LANGUAGE_RECORD_NAME, $tableLanguage); + } + + protected function insertTable($recordName, Piwik_DataTable $table) + { + return $this->getProcessor()->insertBlobRecord($recordName, $table->getSerialized($this->maximumRows, null, Piwik_Archive::INDEX_NB_VISITS)); } public function archivePeriod() @@ -145,7 +146,7 @@ class Piwik_UserSettings_Archiver extends Piwik_PluginsArchiver self::PLUGIN_RECORD_NAME, self::LANGUAGE_RECORD_NAME, ); - $this->getProcessor()->archiveDataTable($dataTableToSum, null, $this->maximumRowsInDataTable); + $this->getProcessor()->archiveDataTable($dataTableToSum, null, $this->maximumRows); } } diff --git a/plugins/VisitTime/Archiver.php b/plugins/VisitTime/Archiver.php index cd19d99c00..af811e0d2f 100644 --- a/plugins/VisitTime/Archiver.php +++ b/plugins/VisitTime/Archiver.php @@ -22,53 +22,49 @@ class Piwik_VisitTime_Archiver extends Piwik_PluginsArchiver protected function aggregateByServerTime() { - $metricsByServerTime = $this->getProcessor()->getMetricsForLabel("HOUR(log_visit.visit_last_action_time)"); - $query = $this->getProcessor()->queryConversionsByDimension("HOUR(log_conversion.server_time)"); - - if ($query === false) return; + $array = $this->getProcessor()->getMetricsForDimension( array("label" => "HOUR(log_visit.visit_last_action_time)" )) ; + $query = $this->getProcessor()->queryConversionsByDimension( array("label" => "HOUR(log_conversion.server_time)") ); + if ($query === false) { + return; + } while ($row = $query->fetch()) { - if (!isset($metricsByServerTime[$row['label']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) { - $metricsByServerTime[$row['label']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $this->getProcessor()->makeEmptyGoalRow($row['idgoal']); - } - $this->getProcessor()->sumGoalMetrics($row, $metricsByServerTime[$row['label']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); + $array->sumMetricsGoals($row['label'], $row); } - $this->getProcessor()->enrichMetricsWithConversions($metricsByServerTime); - - $metricsByServerTime = $this->convertServerTimeToLocalTimezone($metricsByServerTime); - $tableServerTime = $this->getProcessor()->getDataTableFromArray($metricsByServerTime); - $this->makeSureAllHoursAreSet($tableServerTime); - $this->getProcessor()->insertBlobRecord(self::SERVER_TIME_RECORD_NAME, $tableServerTime->getSerialized()); + $array->enrichMetricsWithConversions(); + $array = $this->convertTimeToLocalTimezone($array); + $this->ensureAllHoursAreSet($array); + $this->getProcessor()->insertBlobRecord(self::SERVER_TIME_RECORD_NAME, $this->getProcessor()->getDataTableFromDataArray($array)->getSerialized()); } protected function aggregateByLocalTime() { - $metricsByLocalTime = $this->getProcessor()->getMetricsForLabel("HOUR(log_visit.visitor_localtime)"); - $tableLocalTime = $this->getProcessor()->getDataTableFromArray($metricsByLocalTime); - $this->makeSureAllHoursAreSet($tableLocalTime); - $this->getProcessor()->insertBlobRecord(self::LOCAL_TIME_RECORD_NAME, $tableLocalTime->getSerialized()); + $array = $this->getProcessor()->getMetricsForDimension("HOUR(log_visit.visitor_localtime)"); + $this->ensureAllHoursAreSet($array); + $this->getProcessor()->insertBlobRecord(self::LOCAL_TIME_RECORD_NAME, $this->getProcessor()->getDataTableFromDataArray($array)->getSerialized()); } - protected function convertServerTimeToLocalTimezone($metricsByServerTime) + protected function convertTimeToLocalTimezone(Piwik_DataArray &$array) { $date = Piwik_Date::factory($this->getProcessor()->getStartDatetimeUTC())->toString(); $timezone = $this->getProcessor()->getSite()->getTimezone(); - $visitsByHourTz = array(); - foreach ($metricsByServerTime as $hour => $stats) { + + $converted = array(); + foreach ($array->getDataArray() as $hour => $stats) { $datetime = $date . ' ' . $hour . ':00:00'; $hourInTz = (int)Piwik_Date::factory($datetime, $timezone)->toString('H'); - $visitsByHourTz[$hourInTz] = $stats; + $converted[$hourInTz] = $stats; } - return $visitsByHourTz; + return new Piwik_DataArray($converted); } - private function makeSureAllHoursAreSet($table) + private function ensureAllHoursAreSet( Piwik_DataArray &$array) { + $data = $array->getDataArray(); for ($i = 0; $i <= 23; $i++) { - if ($table->getRowFromLabel($i) === false) { - $row = $this->getProcessor()->makeEmptyRowLabeled($i); - $table->addRow($row); + if (empty($data[$i])) { + $array->sumMetricsVisits( $i, Piwik_DataArray::makeEmptyRow()); } } } diff --git a/plugins/VisitorInterest/Archiver.php b/plugins/VisitorInterest/Archiver.php index f90ea3d2a0..5f0a15d3d0 100644 --- a/plugins/VisitorInterest/Archiver.php +++ b/plugins/VisitorInterest/Archiver.php @@ -90,49 +90,29 @@ class Piwik_VisitorInterest_Archiver extends Piwik_PluginsArchiver self::VISITS_COUNT_RECORD_NAME => 'vbvn', self::DAYS_SINCE_LAST_RECORD_NAME => 'dslv', ); - $row = $this->aggregateFromVisits($prefixes); + $aggregatesMetadata = array( + array('visit_total_time', self::getSecondsGap(), 'log_visit', $prefixes[self::TIME_SPENT_RECORD_NAME]), + array('visit_total_actions', self::$pageGap, 'log_visit', $prefixes[self::PAGES_VIEWED_RECORD_NAME]), + array('visitor_count_visits', self::$visitNumberGap, 'log_visit', $prefixes[self::VISITS_COUNT_RECORD_NAME]), + array('visitor_days_since_last', self::$daysSinceLastVisitGap, 'log_visit', $prefixes[self::DAYS_SINCE_LAST_RECORD_NAME], + $i_am_your_nightmare_DELETE_ME = true + ), + ); + $selects = array(); + foreach($aggregatesMetadata as $aggregateMetadata) { + $selectsFromRangedColumn = Piwik_DataAccess_LogAggregator::getSelectsFromRangedColumn($aggregateMetadata); + $selects = array_merge( $selects, $selectsFromRangedColumn); + } + $query = $this->getProcessor()->queryVisitsByDimension(array(), $where = false, $selects, array()); + $row = $query->fetch(); foreach($prefixes as $recordName => $selectAsPrefix) { - $processor = $this->getProcessor(); - $dataTable = $processor->getSimpleDataTableFromRow($row, Piwik_Archive::INDEX_NB_VISITS, $selectAsPrefix); - $processor->insertBlobRecord($recordName, $dataTable->getSerialized()); + $cleanRow = Piwik_DataAccess_LogAggregator::makeArrayOneColumn($row, Piwik_Archive::INDEX_NB_VISITS, $selectAsPrefix); + $dataTable = Piwik_DataTable::makeFromIndexedArray($cleanRow); + $this->getProcessor()->insertBlobRecord($recordName, $dataTable->getSerialized()); } } - protected function aggregateFromVisits($prefixes) - { - // extra condition for the SQL SELECT that makes sure only returning visits are counted - // when creating the 'days since last visit' report. the SELECT expression below it - // is used to count all new visits. - $daysSinceLastExtraCondition = 'and log_visit.visitor_returning = 1'; - $selectAs = $prefixes[self::DAYS_SINCE_LAST_RECORD_NAME] . 'General_NewVisits'; - $newVisitCountSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) as `$selectAs`"; - - $daysSinceLastVisitSelects = Piwik_DataAccess_LogAggregator::buildReduceByRangeSelect( - 'visitor_days_since_last', self::$daysSinceLastVisitGap, 'log_visit', $prefixes[self::DAYS_SINCE_LAST_RECORD_NAME], - $daysSinceLastExtraCondition); - - // create the select expressions to use - $timeGapSelects = Piwik_DataAccess_LogAggregator::buildReduceByRangeSelect( - 'visit_total_time', self::getSecondsGap(), 'log_visit', $prefixes[self::TIME_SPENT_RECORD_NAME]); - - $pageGapSelects = Piwik_DataAccess_LogAggregator::buildReduceByRangeSelect( - 'visit_total_actions', self::$pageGap, 'log_visit', $prefixes[self::PAGES_VIEWED_RECORD_NAME]); - - $visitsByVisitNumSelects = Piwik_DataAccess_LogAggregator::buildReduceByRangeSelect( - 'visitor_count_visits', self::$visitNumberGap, 'log_visit', $prefixes[self::VISITS_COUNT_RECORD_NAME]); - - - array_unshift($daysSinceLastVisitSelects, $newVisitCountSelect); - - $selects = array_merge( - $timeGapSelects, $pageGapSelects, $visitsByVisitNumSelects, $daysSinceLastVisitSelects); - - // select data for every report - $row = $this->getProcessor()->queryVisitsSimple(implode(',', $selects)); - return $row; - } - /** * Transforms and returns the set of ranges used to calculate the 'visits by total time' * report from ranges in minutes to equivalent ranges in seconds. diff --git a/tests/PHPUnit/Integration/TwoVisitsWithCustomVariables_SegmentMatchVisitorTypeTest.php b/tests/PHPUnit/Integration/TwoVisitsWithCustomVariables_SegmentMatchVisitorTypeTest.php index 17bc92db2d..14d9c78092 100755 --- a/tests/PHPUnit/Integration/TwoVisitsWithCustomVariables_SegmentMatchVisitorTypeTest.php +++ b/tests/PHPUnit/Integration/TwoVisitsWithCustomVariables_SegmentMatchVisitorTypeTest.php @@ -87,6 +87,9 @@ class Test_Piwik_Integration_TwoVisitsWithCustomVariables_SegmentMatchVisitorTyp foreach ($tests as $table => $expectedRows) { $sql = "SELECT count(*) FROM " . Piwik_Common::prefixTable($table); $countBlobs = Zend_Registry::get('db')->fetchOne($sql); + if($expectedRows != $countBlobs) { + var_export(Zend_Registry::get('db')->fetchAll("SELECT * FROM " . Piwik_Common::prefixTable($table))); + } $this->assertEquals($expectedRows, $countBlobs, "$table: %s"); } } -- GitLab