PageRenderTime 47ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/Goals/Archiver.php

https://github.com/CodeYellowBV/piwik
PHP | 418 lines | 326 code | 47 blank | 45 comment | 25 complexity | b153b3deb122a3a1ee724db3e65d90e9 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugins\Goals;
  10. use Piwik\DataAccess\LogAggregator;
  11. use Piwik\DataArray;
  12. use Piwik\DataTable;
  13. use Piwik\Metrics;
  14. use Piwik\PluginsArchiver;
  15. use Piwik\PluginsManager;
  16. use Piwik\Tracker\GoalManager;
  17. class Archiver extends \Piwik\Plugin\Archiver
  18. {
  19. const VISITS_UNTIL_RECORD_NAME = 'visits_until_conv';
  20. const DAYS_UNTIL_CONV_RECORD_NAME = 'days_until_conv';
  21. const ITEMS_SKU_RECORD_NAME = 'Goals_ItemsSku';
  22. const ITEMS_NAME_RECORD_NAME = 'Goals_ItemsName';
  23. const ITEMS_CATEGORY_RECORD_NAME = 'Goals_ItemsCategory';
  24. const SKU_FIELD = 'idaction_sku';
  25. const NAME_FIELD = 'idaction_name';
  26. const CATEGORY_FIELD = 'idaction_category';
  27. const CATEGORY2_FIELD = 'idaction_category2';
  28. const CATEGORY3_FIELD = 'idaction_category3';
  29. const CATEGORY4_FIELD = 'idaction_category4';
  30. const CATEGORY5_FIELD = 'idaction_category5';
  31. const NO_LABEL = ':';
  32. const LOG_CONVERSION_TABLE = 'log_conversion';
  33. const VISITS_COUNT_FIELD = 'visitor_count_visits';
  34. const DAYS_SINCE_FIRST_VISIT_FIELD = 'visitor_days_since_first';
  35. /**
  36. * This array stores the ranges to use when displaying the 'visits to conversion' report
  37. */
  38. public static $visitCountRanges = array(
  39. array(1, 1),
  40. array(2, 2),
  41. array(3, 3),
  42. array(4, 4),
  43. array(5, 5),
  44. array(6, 6),
  45. array(7, 7),
  46. array(8, 8),
  47. array(9, 14),
  48. array(15, 25),
  49. array(26, 50),
  50. array(51, 100),
  51. array(100)
  52. );
  53. /**
  54. * This array stores the ranges to use when displaying the 'days to conversion' report
  55. */
  56. public static $daysToConvRanges = array(
  57. array(0, 0),
  58. array(1, 1),
  59. array(2, 2),
  60. array(3, 3),
  61. array(4, 4),
  62. array(5, 5),
  63. array(6, 6),
  64. array(7, 7),
  65. array(8, 14),
  66. array(15, 30),
  67. array(31, 60),
  68. array(61, 120),
  69. array(121, 364),
  70. array(364)
  71. );
  72. protected $dimensionRecord = array(
  73. self::SKU_FIELD => self::ITEMS_SKU_RECORD_NAME,
  74. self::NAME_FIELD => self::ITEMS_NAME_RECORD_NAME,
  75. self::CATEGORY_FIELD => self::ITEMS_CATEGORY_RECORD_NAME
  76. );
  77. /**
  78. * Array containing one DataArray for each Ecommerce items dimension (name/sku/category abandoned carts and orders)
  79. * @var array
  80. */
  81. protected $itemReports = array();
  82. public function aggregateDayReport()
  83. {
  84. $this->aggregateGeneralGoalMetrics();
  85. $this->aggregateEcommerceItems();
  86. }
  87. protected function aggregateGeneralGoalMetrics()
  88. {
  89. $prefixes = array(
  90. self::VISITS_UNTIL_RECORD_NAME => 'vcv',
  91. self::DAYS_UNTIL_CONV_RECORD_NAME => 'vdsf',
  92. );
  93. $selects = array();
  94. $selects = array_merge($selects, LogAggregator::getSelectsFromRangedColumn(
  95. self::VISITS_COUNT_FIELD, self::$visitCountRanges, self::LOG_CONVERSION_TABLE, $prefixes[self::VISITS_UNTIL_RECORD_NAME]
  96. ));
  97. $selects = array_merge($selects, LogAggregator::getSelectsFromRangedColumn(
  98. self::DAYS_SINCE_FIRST_VISIT_FIELD, self::$daysToConvRanges, self::LOG_CONVERSION_TABLE, $prefixes[self::DAYS_UNTIL_CONV_RECORD_NAME]
  99. ));
  100. $query = $this->getLogAggregator()->queryConversionsByDimension(array(), false, $selects);
  101. if ($query === false) {
  102. return;
  103. }
  104. $totalConversions = $totalRevenue = 0;
  105. $goals = new DataArray();
  106. $visitsToConversions = $daysToConversions = array();
  107. $conversionMetrics = $this->getLogAggregator()->getConversionsMetricFields();
  108. while ($row = $query->fetch()) {
  109. $idGoal = $row['idgoal'];
  110. unset($row['idgoal']);
  111. unset($row['label']);
  112. $values = array();
  113. foreach ($conversionMetrics as $field => $statement) {
  114. $values[$field] = $row[$field];
  115. }
  116. $goals->sumMetrics($idGoal, $values);
  117. if (empty($visitsToConversions[$idGoal])) {
  118. $visitsToConversions[$idGoal] = new DataTable();
  119. }
  120. $array = LogAggregator::makeArrayOneColumn($row, Metrics::INDEX_NB_CONVERSIONS, $prefixes[self::VISITS_UNTIL_RECORD_NAME]);
  121. $visitsToConversions[$idGoal]->addDataTable(DataTable::makeFromIndexedArray($array));
  122. if (empty($daysToConversions[$idGoal])) {
  123. $daysToConversions[$idGoal] = new DataTable();
  124. }
  125. $array = LogAggregator::makeArrayOneColumn($row, Metrics::INDEX_NB_CONVERSIONS, $prefixes[self::DAYS_UNTIL_CONV_RECORD_NAME]);
  126. $daysToConversions[$idGoal]->addDataTable(DataTable::makeFromIndexedArray($array));
  127. // We don't want to sum Abandoned cart metrics in the overall revenue/conversions/converted visits
  128. // since it is a "negative conversion"
  129. if ($idGoal != GoalManager::IDGOAL_CART) {
  130. $totalConversions += $row[Metrics::INDEX_GOAL_NB_CONVERSIONS];
  131. $totalRevenue += $row[Metrics::INDEX_GOAL_REVENUE];
  132. }
  133. }
  134. // Stats by goal, for all visitors
  135. $numericRecords = $this->getConversionsNumericMetrics($goals);
  136. $this->getProcessor()->insertNumericRecords($numericRecords);
  137. $this->insertReports(self::VISITS_UNTIL_RECORD_NAME, $visitsToConversions);
  138. $this->insertReports(self::DAYS_UNTIL_CONV_RECORD_NAME, $daysToConversions);
  139. // Stats for all goals
  140. $nbConvertedVisits = $this->getProcessor()->getNumberOfVisitsConverted();
  141. $metrics = array(
  142. self::getRecordName('conversion_rate') => $this->getConversionRate($nbConvertedVisits),
  143. self::getRecordName('nb_conversions') => $totalConversions,
  144. self::getRecordName('nb_visits_converted') => $nbConvertedVisits,
  145. self::getRecordName('revenue') => $totalRevenue,
  146. );
  147. $this->getProcessor()->insertNumericRecords($metrics);
  148. }
  149. protected function getConversionsNumericMetrics(DataArray $goals)
  150. {
  151. $numericRecords = array();
  152. $goals = $goals->getDataArray();
  153. foreach ($goals as $idGoal => $array) {
  154. foreach ($array as $metricId => $value) {
  155. $metricName = Metrics::$mappingFromIdToNameGoal[$metricId];
  156. $recordName = self::getRecordName($metricName, $idGoal);
  157. $numericRecords[$recordName] = $value;
  158. }
  159. if (!empty($array[Metrics::INDEX_GOAL_NB_VISITS_CONVERTED])) {
  160. $conversion_rate = $this->getConversionRate($array[Metrics::INDEX_GOAL_NB_VISITS_CONVERTED]);
  161. $recordName = self::getRecordName('conversion_rate', $idGoal);
  162. $numericRecords[$recordName] = $conversion_rate;
  163. }
  164. }
  165. return $numericRecords;
  166. }
  167. /**
  168. * @param string $recordName 'nb_conversions'
  169. * @param int|bool $idGoal idGoal to return the metrics for, or false to return overall
  170. * @return string Archive record name
  171. */
  172. static public function getRecordName($recordName, $idGoal = false)
  173. {
  174. $idGoalStr = '';
  175. if ($idGoal !== false) {
  176. $idGoalStr = $idGoal . "_";
  177. }
  178. return 'Goal_' . $idGoalStr . $recordName;
  179. }
  180. protected function getConversionRate($count)
  181. {
  182. $visits = $this->getProcessor()->getNumberOfVisits();
  183. return round(100 * $count / $visits, GoalManager::REVENUE_PRECISION);
  184. }
  185. protected function insertReports($recordName, $visitsToConversions)
  186. {
  187. foreach ($visitsToConversions as $idGoal => $table) {
  188. $record = self::getRecordName($recordName, $idGoal);
  189. $this->getProcessor()->insertBlobRecord($record, $table->getSerialized());
  190. }
  191. $overviewTable = $this->getOverviewFromGoalTables($visitsToConversions);
  192. $this->getProcessor()->insertBlobRecord(self::getRecordName($recordName), $overviewTable->getSerialized());
  193. }
  194. protected function getOverviewFromGoalTables($tableByGoal)
  195. {
  196. $overview = new DataTable();
  197. foreach ($tableByGoal as $idGoal => $table) {
  198. if ($this->isStandardGoal($idGoal)) {
  199. $overview->addDataTable($table);
  200. }
  201. }
  202. return $overview;
  203. }
  204. protected function isStandardGoal($idGoal)
  205. {
  206. return !in_array($idGoal, $this->getEcommerceIdGoals());
  207. }
  208. protected function aggregateEcommerceItems()
  209. {
  210. $this->initItemReports();
  211. foreach ($this->getItemsDimensions() as $dimension) {
  212. $query = $this->getLogAggregator()->queryEcommerceItems($dimension);
  213. if ($query == false) {
  214. continue;
  215. }
  216. $this->aggregateFromEcommerceItems($query, $dimension);
  217. }
  218. $this->insertItemReports();
  219. return true;
  220. }
  221. protected function initItemReports()
  222. {
  223. foreach ($this->getEcommerceIdGoals() as $ecommerceType) {
  224. foreach ($this->dimensionRecord as $dimension => $record) {
  225. $this->itemReports[$dimension][$ecommerceType] = new DataArray();
  226. }
  227. }
  228. }
  229. protected function insertItemReports()
  230. {
  231. /** @var DataArray $array */
  232. foreach ($this->itemReports as $dimension => $itemAggregatesByType) {
  233. foreach ($itemAggregatesByType as $ecommerceType => $itemAggregate) {
  234. $recordName = $this->dimensionRecord[$dimension];
  235. if ($ecommerceType == GoalManager::IDGOAL_CART) {
  236. $recordName = self::getItemRecordNameAbandonedCart($recordName);
  237. }
  238. $table = $itemAggregate->asDataTable();
  239. $this->getProcessor()->insertBlobRecord($recordName, $table->getSerialized());
  240. }
  241. }
  242. }
  243. protected function getItemsDimensions()
  244. {
  245. $dimensions = array_keys($this->dimensionRecord);
  246. foreach ($this->getItemExtraCategories() as $category) {
  247. $dimensions[] = $category;
  248. }
  249. return $dimensions;
  250. }
  251. protected function getItemExtraCategories()
  252. {
  253. return array(self::CATEGORY2_FIELD, self::CATEGORY3_FIELD, self::CATEGORY4_FIELD, self::CATEGORY5_FIELD);
  254. }
  255. protected function isItemExtraCategory($field)
  256. {
  257. return in_array($field, $this->getItemExtraCategories());
  258. }
  259. protected function aggregateFromEcommerceItems($query, $dimension)
  260. {
  261. while ($row = $query->fetch()) {
  262. $ecommerceType = $row['ecommerceType'];
  263. $label = $this->cleanupRowGetLabel($row, $dimension);
  264. if ($label === false) {
  265. continue;
  266. }
  267. // Aggregate extra categories in the Item categories array
  268. if ($this->isItemExtraCategory($dimension)) {
  269. $array = $this->itemReports[self::CATEGORY_FIELD][$ecommerceType];
  270. } else {
  271. $array = $this->itemReports[$dimension][$ecommerceType];
  272. }
  273. $this->roundColumnValues($row);
  274. $array->sumMetrics($label, $row);
  275. }
  276. }
  277. protected function cleanupRowGetLabel(&$row, $currentField)
  278. {
  279. $label = $row['label'];
  280. if (empty($label)) {
  281. // An empty additional category -> skip this iteration
  282. if ($this->isItemExtraCategory($currentField)) {
  283. return false;
  284. }
  285. $label = "Value not defined";
  286. // Product Name/Category not defined"
  287. if (\Piwik\Plugin\Manager::getInstance()->isPluginActivated('CustomVariables')) {
  288. $label = \Piwik\Plugins\CustomVariables\Archiver::LABEL_CUSTOM_VALUE_NOT_DEFINED;
  289. }
  290. }
  291. if ($row['ecommerceType'] == GoalManager::IDGOAL_CART) {
  292. // abandoned carts are the numner of visits with an abandoned cart
  293. $row[Metrics::INDEX_ECOMMERCE_ORDERS] = $row[Metrics::INDEX_NB_VISITS];
  294. }
  295. unset($row[Metrics::INDEX_NB_VISITS]);
  296. unset($row['label']);
  297. unset($row['labelIdAction']);
  298. unset($row['ecommerceType']);
  299. return $label;
  300. }
  301. protected function roundColumnValues(&$row)
  302. {
  303. $columnsToRound = array(
  304. Metrics::INDEX_ECOMMERCE_ITEM_REVENUE,
  305. Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY,
  306. Metrics::INDEX_ECOMMERCE_ITEM_PRICE,
  307. Metrics::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED,
  308. );
  309. foreach ($columnsToRound as $column) {
  310. if (isset($row[$column])
  311. && $row[$column] == round($row[$column])
  312. ) {
  313. $row[$column] = round($row[$column]);
  314. }
  315. }
  316. }
  317. protected function getEcommerceIdGoals()
  318. {
  319. return array(GoalManager::IDGOAL_CART, GoalManager::IDGOAL_ORDER);
  320. }
  321. static public function getItemRecordNameAbandonedCart($recordName)
  322. {
  323. return $recordName . '_Cart';
  324. }
  325. /**
  326. * @internal param $this->getProcessor()
  327. */
  328. public function aggregateMultipleReports()
  329. {
  330. /*
  331. * Archive Ecommerce Items
  332. */
  333. $dataTableToSum = $this->dimensionRecord;
  334. foreach ($this->dimensionRecord as $recordName) {
  335. $dataTableToSum[] = self::getItemRecordNameAbandonedCart($recordName);
  336. }
  337. $this->getProcessor()->aggregateDataTableRecords($dataTableToSum);
  338. /*
  339. * Archive General Goal metrics
  340. */
  341. $goalIdsToSum = GoalManager::getGoalIds($this->getProcessor()->getParams()->getSite()->getId());
  342. //Ecommerce
  343. $goalIdsToSum[] = GoalManager::IDGOAL_ORDER;
  344. $goalIdsToSum[] = GoalManager::IDGOAL_CART; //bug here if idgoal=1
  345. // Overall goal metrics
  346. $goalIdsToSum[] = false;
  347. $fieldsToSum = array();
  348. foreach ($goalIdsToSum as $goalId) {
  349. $metricsToSum = Goals::getGoalColumns($goalId);
  350. unset($metricsToSum[array_search('conversion_rate', $metricsToSum)]);
  351. foreach ($metricsToSum as $metricName) {
  352. $fieldsToSum[] = self::getRecordName($metricName, $goalId);
  353. }
  354. }
  355. $records = $this->getProcessor()->aggregateNumericMetrics($fieldsToSum);
  356. // also recording conversion_rate for each goal
  357. foreach ($goalIdsToSum as $goalId) {
  358. $nb_conversions = $records[self::getRecordName('nb_visits_converted', $goalId)];
  359. $conversion_rate = $this->getConversionRate($nb_conversions);
  360. $this->getProcessor()->insertNumericRecord(self::getRecordName('conversion_rate', $goalId), $conversion_rate);
  361. // sum up the visits to conversion data table & the days to conversion data table
  362. $this->getProcessor()->aggregateDataTableRecords(array(
  363. self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $goalId),
  364. self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $goalId)));
  365. }
  366. // sum up goal overview reports
  367. $this->getProcessor()->aggregateDataTableRecords(array(
  368. self::getRecordName(self::VISITS_UNTIL_RECORD_NAME),
  369. self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME)));
  370. }
  371. }