2023-01-23 11:03:31 +01:00

432 lines
16 KiB
PHP

<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\Goals;
use Piwik\DataAccess\LogAggregator;
use Piwik\DataArray;
use Piwik\DataTable;
use Piwik\Metrics;
use Piwik\Tracker\GoalManager;
use Piwik\Plugins\VisitFrequency\API as VisitFrequencyAPI;
class Archiver extends \Piwik\Plugin\Archiver
{
const VISITS_UNTIL_RECORD_NAME = 'visits_until_conv';
const DAYS_UNTIL_CONV_RECORD_NAME = 'days_until_conv';
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';
const NEW_VISIT_SEGMENT = 'visitorType%3D%3Dnew'; // visitorType==new
/**
* This array stores the ranges to use when displaying the 'visits to conversion' report
*/
public static $visitCountRanges = array(
array(1, 1),
array(2, 2),
array(3, 3),
array(4, 4),
array(5, 5),
array(6, 6),
array(7, 7),
array(8, 8),
array(9, 14),
array(15, 25),
array(26, 50),
array(51, 100),
array(100)
);
/**
* This array stores the ranges to use when displaying the 'days to conversion' report
*/
public static $daysToConvRanges = array(
array(0, 0),
array(1, 1),
array(2, 2),
array(3, 3),
array(4, 4),
array(5, 5),
array(6, 6),
array(7, 7),
array(8, 14),
array(15, 30),
array(31, 60),
array(61, 120),
array(121, 364),
array(364)
);
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 aggregateDayReport()
{
$this->aggregateGeneralGoalMetrics();
$this->aggregateEcommerceItems();
$this->getProcessor()->processDependentArchive('Goals', API::NEW_VISIT_SEGMENT);
$this->getProcessor()->processDependentArchive('Goals', VisitFrequencyAPI::RETURNING_VISITOR_SEGMENT);
}
protected function aggregateGeneralGoalMetrics()
{
$prefixes = array(
self::VISITS_UNTIL_RECORD_NAME => 'vcv',
self::DAYS_UNTIL_CONV_RECORD_NAME => 'vdsf',
);
$selects = array();
$selects = array_merge($selects, LogAggregator::getSelectsFromRangedColumn(
self::VISITS_COUNT_FIELD, self::$visitCountRanges, self::LOG_CONVERSION_TABLE, $prefixes[self::VISITS_UNTIL_RECORD_NAME]
));
$selects = array_merge($selects, LogAggregator::getSelectsFromRangedColumn(
self::DAYS_SINCE_FIRST_VISIT_FIELD, self::$daysToConvRanges, self::LOG_CONVERSION_TABLE, $prefixes[self::DAYS_UNTIL_CONV_RECORD_NAME]
));
$query = $this->getLogAggregator()->queryConversionsByDimension(array(), false, $selects);
if ($query === false) {
return;
}
$totalConversions = $totalRevenue = 0;
$goals = new DataArray();
$visitsToConversions = $daysToConversions = array();
$conversionMetrics = $this->getLogAggregator()->getConversionsMetricFields();
while ($row = $query->fetch()) {
$idGoal = $row['idgoal'];
unset($row['idgoal']);
unset($row['label']);
$values = array();
foreach ($conversionMetrics as $field => $statement) {
$values[$field] = $row[$field];
}
$goals->sumMetrics($idGoal, $values);
if (empty($visitsToConversions[$idGoal])) {
$visitsToConversions[$idGoal] = new DataTable();
}
$array = LogAggregator::makeArrayOneColumn($row, Metrics::INDEX_NB_CONVERSIONS, $prefixes[self::VISITS_UNTIL_RECORD_NAME]);
$visitsToConversions[$idGoal]->addDataTable(DataTable::makeFromIndexedArray($array));
if (empty($daysToConversions[$idGoal])) {
$daysToConversions[$idGoal] = new DataTable();
}
$array = LogAggregator::makeArrayOneColumn($row, Metrics::INDEX_NB_CONVERSIONS, $prefixes[self::DAYS_UNTIL_CONV_RECORD_NAME]);
$daysToConversions[$idGoal]->addDataTable(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 != GoalManager::IDGOAL_CART) {
$totalConversions += $row[Metrics::INDEX_GOAL_NB_CONVERSIONS];
$totalRevenue += $row[Metrics::INDEX_GOAL_REVENUE];
}
}
// Stats by goal, for all visitors
$numericRecords = $this->getConversionsNumericMetrics($goals);
$this->getProcessor()->insertNumericRecords($numericRecords);
$this->insertReports(self::VISITS_UNTIL_RECORD_NAME, $visitsToConversions);
$this->insertReports(self::DAYS_UNTIL_CONV_RECORD_NAME, $daysToConversions);
// Stats for all goals
$nbConvertedVisits = $this->getProcessor()->getNumberOfVisitsConverted();
$metrics = array(
self::getRecordName('nb_conversions') => $totalConversions,
self::getRecordName('nb_visits_converted') => $nbConvertedVisits,
self::getRecordName('revenue') => $totalRevenue,
);
$this->getProcessor()->insertNumericRecords($metrics);
}
protected function getConversionsNumericMetrics(DataArray $goals)
{
$numericRecords = array();
$goals = $goals->getDataArray();
foreach ($goals as $idGoal => $array) {
foreach ($array as $metricId => $value) {
$metricName = Metrics::$mappingFromIdToNameGoal[$metricId];
$recordName = self::getRecordName($metricName, $idGoal);
$numericRecords[$recordName] = $value;
}
}
return $numericRecords;
}
/**
* @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
*/
public static function getRecordName($recordName, $idGoal = false)
{
$idGoalStr = '';
if ($idGoal !== false) {
$idGoalStr = $idGoal . "_";
}
return 'Goal_' . $idGoalStr . $recordName;
}
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 DataTable();
foreach ($tableByGoal as $idGoal => $table) {
if ($this->isStandardGoal($idGoal)) {
$overview->addDataTable($table);
}
}
return $overview;
}
protected function isStandardGoal($idGoal)
{
return !in_array($idGoal, $this->getEcommerceIdGoals());
}
protected function aggregateEcommerceItems()
{
$this->initItemReports();
foreach ($this->getItemsDimensions() as $dimension) {
$query = $this->getLogAggregator()->queryEcommerceItems($dimension);
if ($query == false) {
continue;
}
$this->aggregateFromEcommerceItems($query, $dimension);
}
$this->insertItemReports();
return true;
}
protected function initItemReports()
{
foreach ($this->getEcommerceIdGoals() as $ecommerceType) {
foreach ($this->dimensionRecord as $dimension => $record) {
$this->itemReports[$dimension][$ecommerceType] = new DataArray();
}
}
}
protected function insertItemReports()
{
/** @var DataArray $array */
foreach ($this->itemReports as $dimension => $itemAggregatesByType) {
foreach ($itemAggregatesByType as $ecommerceType => $itemAggregate) {
$recordName = $this->dimensionRecord[$dimension];
if ($ecommerceType == GoalManager::IDGOAL_CART) {
$recordName = self::getItemRecordNameAbandonedCart($recordName);
}
$table = $itemAggregate->asDataTable();
$this->getProcessor()->insertBlobRecord($recordName, $table->getSerialized());
}
}
}
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 (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomVariables')) {
$label = \Piwik\Plugins\CustomVariables\Archiver::LABEL_CUSTOM_VALUE_NOT_DEFINED;
}
}
if ($row['ecommerceType'] == GoalManager::IDGOAL_CART) {
// abandoned carts are the numner of visits with an abandoned cart
$row[Metrics::INDEX_ECOMMERCE_ORDERS] = $row[Metrics::INDEX_NB_VISITS];
}
unset($row[Metrics::INDEX_NB_VISITS]);
unset($row['label']);
unset($row['labelIdAction']);
unset($row['ecommerceType']);
return $label;
}
protected function roundColumnValues(&$row)
{
$columnsToRound = array(
Metrics::INDEX_ECOMMERCE_ITEM_REVENUE,
Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY,
Metrics::INDEX_ECOMMERCE_ITEM_PRICE,
Metrics::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(GoalManager::IDGOAL_CART, GoalManager::IDGOAL_ORDER);
}
public static function getItemRecordNameAbandonedCart($recordName)
{
return $recordName . '_Cart';
}
/**
* @internal param $this->getProcessor()
*/
public function aggregateMultipleReports()
{
/*
* Archive Ecommerce Items
*/
$dataTableToSum = $this->dimensionRecord;
foreach ($this->dimensionRecord as $recordName) {
$dataTableToSum[] = self::getItemRecordNameAbandonedCart($recordName);
}
$columnsAggregationOperation = null;
$this->getProcessor()->aggregateDataTableRecords($dataTableToSum,
$maximumRowsInDataTableLevelZero = null,
$maximumRowsInSubDataTable = null,
$columnToSortByBeforeTruncation = null,
$columnsAggregationOperation,
$columnsToRenameAfterAggregation = null,
$countRowsRecursive = array());
/*
* Archive General Goal metrics
*/
$goalIdsToSum = GoalManager::getGoalIds($this->getProcessor()->getParams()->getSite()->getId());
//Ecommerce
$goalIdsToSum[] = GoalManager::IDGOAL_ORDER;
$goalIdsToSum[] = GoalManager::IDGOAL_CART; //bug here if idgoal=1
// Overall goal metrics
$goalIdsToSum[] = false;
$fieldsToSum = array();
foreach ($goalIdsToSum as $goalId) {
$metricsToSum = Goals::getGoalColumns($goalId);
foreach ($metricsToSum as $metricName) {
$fieldsToSum[] = self::getRecordName($metricName, $goalId);
}
}
$this->getProcessor()->aggregateNumericMetrics($fieldsToSum);
$columnsAggregationOperation = null;
foreach ($goalIdsToSum as $goalId) {
// sum up the visits to conversion data table & the days to conversion data table
$this->getProcessor()->aggregateDataTableRecords(
array(self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $goalId),
self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $goalId)),
$maximumRowsInDataTableLevelZero = null,
$maximumRowsInSubDataTable = null,
$columnToSortByBeforeTruncation = null,
$columnsAggregationOperation,
$columnsToRenameAfterAggregation = null,
$countRowsRecursive = array());
}
$columnsAggregationOperation = null;
// sum up goal overview reports
$this->getProcessor()->aggregateDataTableRecords(
array(self::getRecordName(self::VISITS_UNTIL_RECORD_NAME),
self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME)),
$maximumRowsInDataTableLevelZero = null,
$maximumRowsInSubDataTable = null,
$columnToSortByBeforeTruncation = null,
$columnsAggregationOperation,
$columnsToRenameAfterAggregation = null,
$countRowsRecursive = array());
$this->getProcessor()->processDependentArchive('Goals', API::NEW_VISIT_SEGMENT);
$this->getProcessor()->processDependentArchive('Goals', VisitFrequencyAPI::RETURNING_VISITOR_SEGMENT);
}
}