PageRenderTime 27ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 1ms

/core/ArchiveProcessing/Day.php

https://github.com/quarkness/piwik
PHP | 877 lines | 510 code | 89 blank | 278 comment | 51 complexity | 2ed70d9e4a8d8c648f293e9447362b58 MD5 | raw file
  1. <?php
  2. /**
  3. * Piwik - Open source web analytics
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. * @version $Id$
  8. *
  9. * @category Piwik
  10. * @package Piwik
  11. */
  12. /**
  13. * Handles the archiving process for a day.
  14. * The class provides generic helper methods to manipulate data from the DB,
  15. * easily create Piwik_DataTable objects from running SELECT ... GROUP BY on the log_visit table.
  16. *
  17. * All the logic of the archiving is done inside the plugins listening to the event 'ArchiveProcessing_Day.compute'
  18. *
  19. * @package Piwik
  20. * @subpackage Piwik_ArchiveProcessing
  21. */
  22. class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing
  23. {
  24. function __construct()
  25. {
  26. parent::__construct();
  27. $this->db = Zend_Registry::get('db');
  28. }
  29. /**
  30. * Main method to process logs for a day. The only logic done here is computing the number of visits, actions, etc.
  31. * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Day.compute'.
  32. * See some of the plugins for an example eg. 'Provider'
  33. */
  34. protected function compute()
  35. {
  36. if(!$this->isThereSomeVisits())
  37. {
  38. return;
  39. }
  40. Piwik_PostEvent('ArchiveProcessing_Day.compute', $this);
  41. }
  42. /**
  43. * Returns true if there are logs for the current archive.
  44. *
  45. * If the current archive is for a specific plugin (for example, Referers),
  46. * (for example when a Segment is defined and the Keywords report is requested)
  47. * Then the function will create the Archive for the Core metrics 'VisitsSummary' which will in turn process the number of visits
  48. *
  49. * If there is no specified segment, the SQL query will always run.
  50. */
  51. public function isThereSomeVisits()
  52. {
  53. if (!is_null($this->isThereSomeVisits))
  54. {
  55. if ($this->isThereSomeVisits && is_null($this->nb_visits))
  56. {
  57. debug_print_backtrace();
  58. exit;
  59. }
  60. return $this->isThereSomeVisits;
  61. }
  62. // prepare segmentation
  63. $segment = $this->getSegment();
  64. // We check if there is visits for the requested date / site / segment
  65. // If no specified Segment
  66. // Or if a segment is passed and we specifically process VisitsSummary
  67. // Then we check the logs. This is to ensure that this query is ran only once for this day/site/segment (rather than running it for every plugin)
  68. $reportType = self::getPluginBeingProcessed($this->getRequestedReport());
  69. if ($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period)
  70. || ($reportType == 'VisitsSummary'))
  71. {
  72. // build query parts
  73. $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors,
  74. count(*) as nb_visits,
  75. sum(log_visit.visit_total_actions) as nb_actions,
  76. max(log_visit.visit_total_actions) as max_actions,
  77. sum(log_visit.visit_total_time) as sum_visit_length,
  78. sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as bounce_count,
  79. sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted
  80. ";
  81. $from = "log_visit";
  82. $where = "log_visit.visit_last_action_time >= ?
  83. AND log_visit.visit_last_action_time <= ?
  84. AND log_visit.idsite = ?
  85. ";
  86. $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite);
  87. $query = $segment->getSelectQuery($select, $from, $where, $bind);
  88. $bind = $query['bind'];
  89. $sql = $query['sql'];
  90. $data = $this->db->fetchRow($sql, $bind);
  91. // no visits found
  92. if (!is_array($data) || $data['nb_visits'] == 0)
  93. {
  94. return $this->isThereSomeVisits = false;
  95. }
  96. // visits found: set attribtues
  97. foreach ($data as $name => $value)
  98. {
  99. $this->insertNumericRecord($name, $value);
  100. }
  101. $this->setNumberOfVisits($data['nb_visits']);
  102. $this->setNumberOfVisitsConverted($data['nb_visits_converted']);
  103. return $this->isThereSomeVisits = true;
  104. }
  105. return $this->redirectRequestToVisitsSummary();
  106. }
  107. /**
  108. * If a segment is specified but a plugin other than 'VisitsSummary' is being requested,
  109. * we create an archive for processing VisitsSummary Core Metrics, which will in turn
  110. * execute the query above (in isThereSomeVisits)
  111. */
  112. private function redirectRequestToVisitsSummary()
  113. {
  114. $archive = new Piwik_Archive_Single();
  115. $archive->setSite($this->site);
  116. $archive->setPeriod($this->period);
  117. $archive->setSegment($this->getSegment());
  118. $archive->setRequestedReport('VisitsSummary');
  119. $nbVisits = $archive->getNumeric('nb_visits');
  120. $this->isThereSomeVisits = $nbVisits > 0;
  121. if ($this->isThereSomeVisits)
  122. {
  123. $nbVisitsConverted = $archive->getNumeric('nb_visits_converted');
  124. $this->setNumberOfVisits($nbVisits);
  125. $this->setNumberOfVisitsConverted($nbVisitsConverted);
  126. }
  127. return $this->isThereSomeVisits;
  128. }
  129. /**
  130. * Creates and returns an array of SQL SELECT expressions that will summarize
  131. * the data in a column of a specified table, over a set of ranges.
  132. *
  133. * The SELECT expressions will count the number of column values that are
  134. * within each range.
  135. *
  136. * @param string $column The column of the log_conversion table to reduce.
  137. * @param array $ranges The ranges to reduce data over.
  138. * @param string $table The table the SELECTs should use.
  139. * @param string $selectColumnPrefix The prefix when specifying what a SELECT
  140. * expression will be selected AS.
  141. * @param string $extraCondition An extra condition to be appended to 'case when'
  142. * expressions. Must start with the logical operator,
  143. * ie (AND, OR, etc.).
  144. * @return array An array of SQL SELECT expressions.
  145. */
  146. public static function buildReduceByRangeSelect(
  147. $column, $ranges, $table, $selectColumnPrefix = '', $extraCondition = false)
  148. {
  149. $selects = array();
  150. foreach($ranges as $gap)
  151. {
  152. if (count($gap) == 2)
  153. {
  154. $lowerBound = $gap[0];
  155. $upperBound = $gap[1];
  156. $selectAs = "$selectColumnPrefix$lowerBound-$upperBound";
  157. $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition".
  158. " then 1 else 0 end) as `$selectAs`";
  159. }
  160. else
  161. {
  162. $lowerBound = $gap[0];
  163. $selectAs = $selectColumnPrefix.($lowerBound + 1).urlencode('+');
  164. $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`";
  165. }
  166. }
  167. return $selects;
  168. }
  169. /**
  170. * Converts a database SELECT result into a whole DataTable with two columns and as many
  171. * rows as elements in $row.
  172. *
  173. * The key of each element in $row is used as the value of the first column, and the
  174. * value of each element is used as the second column.
  175. *
  176. * NOTE: $selectAsPrefix can be used to make sure only SOME of the data in $row is used.
  177. *
  178. * @param array $row The database row to convert.
  179. * @param mixed $labelCount The label to use for the second column of the DataTable result.
  180. * @param string $selectAsPrefix A string that identifies which elements of $row to use
  181. * in the result. Every key of $row that starts with this
  182. * value is used.
  183. * @return Piwik_DataTable
  184. */
  185. public function getSimpleDataTableFromRow($row, $labelCount, $selectAsPrefix = '')
  186. {
  187. // the labels in $row can have prefixes that need to be removed before creating a table
  188. $cleanRow = array();
  189. foreach($row as $label => $count)
  190. {
  191. if (empty($selectAsPrefix) || strpos($label, $selectAsPrefix) === 0)
  192. {
  193. $cleanLabel = substr($label, strlen($selectAsPrefix));
  194. $cleanRow[$cleanLabel] = array($labelCount => $count);
  195. }
  196. }
  197. $table = new Piwik_DataTable();
  198. $table->addRowsFromArrayWithIndexLabel($cleanRow);
  199. return $table;
  200. }
  201. /**
  202. * Performs a simple query on the log_visit table within the time range this archive
  203. * represents.
  204. *
  205. * @param string $select The SELECT clause.
  206. * @param string|bool $orderBy The ORDER BY clause (without the 'ORDER BY' part). Set to
  207. * false to specify no ORDER BY.
  208. * @param array|bool $groupByCols An array of column names to group by. Set to false to
  209. * specify no GROUP BY.
  210. * @param bool $oneResultRow Whether only one row is expected or not. If set to true,
  211. * this function returns one row, if false, an array of rows.
  212. * @return array
  213. */
  214. public function queryVisitsSimple($select, $orderBy = false, $groupByCols = false, $oneResultRow = true)
  215. {
  216. $from = "log_visit";
  217. $where = "log_visit.visit_last_action_time >= ?
  218. AND log_visit.visit_last_action_time <= ?
  219. AND log_visit.idsite = ?";
  220. $groupBy = false;
  221. if ($groupByCols and !empty($groupByCols))
  222. {
  223. $groupBy = implode(',', $groupByCols);
  224. }
  225. $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite);
  226. $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy);
  227. if ($oneResultRow)
  228. {
  229. return $this->db->fetchRow($query['sql'], $query['bind']);
  230. }
  231. else
  232. {
  233. return $this->db->fetchAll($query['sql'], $query['bind']);
  234. }
  235. }
  236. /**
  237. * Helper function that returns a DataTable containing the $select fields / value pairs.
  238. * IMPORTANT: The $select must return only one row!!
  239. *
  240. * Example $select = "count(distinct( config_os )) as countDistinctOs,
  241. * sum( config_flash ) / count(distinct(idvisit)) as percentFlash "
  242. * $labelCount = "test_column_name"
  243. * will return a dataTable that looks like
  244. * label test_column_name
  245. * CountDistinctOs 9
  246. * PercentFlash 0.5676
  247. *
  248. *
  249. * @param string $select
  250. * @param string $labelCount
  251. * @return Piwik_DataTable
  252. */
  253. public function getSimpleDataTableFromSelect($select, $labelCount)
  254. {
  255. $data = $this->queryVisitsSimple($select);
  256. return $this->getSimpleDataTableFromRow($data, $labelCount);
  257. }
  258. public function queryActionsByDimension($label, $where = '')
  259. {
  260. if(is_array($label))
  261. {
  262. $label2 = $label;
  263. foreach($label2 as &$field) { $field = 'log_link_visit_action.'. $field; }
  264. $groupBy = implode(", ", $label2);
  265. foreach($label2 as $id => &$field) { $field = "$field AS ".$label[$id]; }
  266. $select = implode(", ", $label2);
  267. // IF we query Custom Variables scope "page" either: Product SKU, Product Name,
  268. // then we also query the "Product page view" price which was possibly recorded.
  269. if(in_array(reset($label), array('custom_var_k3','custom_var_k4','custom_var_k5')))
  270. {
  271. $select .= ", ".self::getSqlRevenue("AVG(log_link_visit_action.custom_var_v2)")." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED ."`";
  272. }
  273. }
  274. else
  275. {
  276. $select = $label . " AS label ";
  277. $groupBy = 'label';
  278. }
  279. if(!empty($where))
  280. {
  281. $where = sprintf($where, "log_link_visit_action", "log_link_visit_action");
  282. $where = ' AND '.$where;
  283. }
  284. /*
  285. * Page URLs and Page names, general stats
  286. */
  287. $select = "$select,
  288. count(distinct log_link_visit_action.idvisit) as `". Piwik_Archive::INDEX_NB_VISITS ."`,
  289. count(distinct log_link_visit_action.idvisitor) as `". Piwik_Archive::INDEX_NB_UNIQ_VISITORS ."`,
  290. count(*) as `". Piwik_Archive::INDEX_NB_ACTIONS ."`";
  291. $from = "log_link_visit_action";
  292. $where = "log_link_visit_action.server_time >= ?
  293. AND log_link_visit_action.server_time <= ?
  294. AND log_link_visit_action.idsite = ?
  295. $where";
  296. $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite);
  297. $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy=false, $groupBy);
  298. return $this->db->query($query['sql'], $query['bind']);
  299. }
  300. /**
  301. * Query visits by dimension
  302. *
  303. * @param string $label mixed Can be a string, eg. "referer_name", will be aliased as 'label' in the returned rows
  304. * Can also be an array of strings, when the dimension spans multiple fields, eg. array("referer_name", "referer_keyword")
  305. * @param string $where Additional condition for WHERE clause
  306. */
  307. public function queryVisitsByDimension($label, $where = '')
  308. {
  309. if(is_array($label))
  310. {
  311. $groupBy = "log_visit.".implode(", log_visit.", $label);
  312. foreach($label as &$field)
  313. {
  314. $field = 'log_visit.'.$field.' AS '.$field;
  315. }
  316. $select = implode(", ", $label);
  317. }
  318. else
  319. {
  320. $select = $label . " AS label ";
  321. $groupBy = 'label';
  322. }
  323. if(!empty($where))
  324. {
  325. $where = sprintf($where, "log_visit", "log_visit");
  326. $where = ' AND '.$where;
  327. }
  328. $select = "$select,
  329. count(distinct log_visit.idvisitor) as `". Piwik_Archive::INDEX_NB_UNIQ_VISITORS ."`,
  330. count(*) as `". Piwik_Archive::INDEX_NB_VISITS ."`,
  331. sum(log_visit.visit_total_actions) as `". Piwik_Archive::INDEX_NB_ACTIONS ."`,
  332. max(log_visit.visit_total_actions) as `". Piwik_Archive::INDEX_MAX_ACTIONS ."`,
  333. sum(log_visit.visit_total_time) as `". Piwik_Archive::INDEX_SUM_VISIT_LENGTH ."`,
  334. sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `". Piwik_Archive::INDEX_BOUNCE_COUNT ."`,
  335. sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as `". Piwik_Archive::INDEX_NB_VISITS_CONVERTED ."`";
  336. $from = "log_visit";
  337. $where = "log_visit.visit_last_action_time >= ?
  338. AND log_visit.visit_last_action_time <= ?
  339. AND log_visit.idsite = ?
  340. $where";
  341. $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite);
  342. $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy=false, $groupBy);
  343. return $this->db->query($query['sql'], $query['bind']);
  344. }
  345. /**
  346. * @see queryVisitsByDimension() Similar to this function,
  347. * but queries metrics for the requested dimensions,
  348. * for each Goal conversion
  349. */
  350. public function queryConversionsByDimension($label, $where = '', $aggregateLabels = array())
  351. {
  352. if(empty($label))
  353. {
  354. $select = "";
  355. $groupBy = "";
  356. }
  357. elseif(is_array($label))
  358. {
  359. $groupBy = "log_conversion.".implode(", log_conversion.", $label);
  360. foreach($label as &$field)
  361. {
  362. $field = 'log_conversion.'.$field.' AS '.$field ;
  363. }
  364. $select = implode(", ", $label) . ", ";
  365. }
  366. else
  367. {
  368. $select = $label . " AS label, ";
  369. $groupBy = 'label';
  370. }
  371. if(!empty($aggregateLabels))
  372. {
  373. $select .= implode(", ", $aggregateLabels) . ", ";
  374. }
  375. if(!empty($where))
  376. {
  377. $where = sprintf($where, "log_conversion", "log_conversion");
  378. $where = ' AND '.$where;
  379. }
  380. $select .= self::getSqlRevenue('SUM(log_conversion.revenue_subtotal)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL ."`,".
  381. self::getSqlRevenue('SUM(log_conversion.revenue_tax)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX ."`,".
  382. self::getSqlRevenue('SUM(log_conversion.revenue_shipping)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING ."`,".
  383. self::getSqlRevenue('SUM(log_conversion.revenue_discount)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT ."`,".
  384. "SUM(log_conversion.items) as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS ."`, ";
  385. $groupBy = !empty($groupBy) ? ", $groupBy" : '';
  386. $select = "$select
  387. log_conversion.idgoal,
  388. count(*) as `". Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS ."`,
  389. ".self::getSqlRevenue('SUM(log_conversion.revenue)')." as `". Piwik_Archive::INDEX_GOAL_REVENUE ."`,
  390. count(distinct log_conversion.idvisit) as `". Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED."`";
  391. $from = "log_conversion";
  392. $where = "log_conversion.server_time >= ?
  393. AND log_conversion.server_time <= ?
  394. AND log_conversion.idsite = ?
  395. $where";
  396. $groupBy = "log_conversion.idgoal $groupBy";
  397. $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite);
  398. $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy=false, $groupBy);
  399. return $this->db->query($query['sql'], $query['bind']);
  400. }
  401. public function queryEcommerceItems($field)
  402. {
  403. $query = "SELECT
  404. name as label,
  405. ".self::getSqlRevenue('SUM(quantity * price)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE ."`,
  406. ".self::getSqlRevenue('SUM(quantity)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY ."`,
  407. ".self::getSqlRevenue('SUM(price)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE ."`,
  408. count(distinct idorder) as `". Piwik_Archive::INDEX_ECOMMERCE_ORDERS."`,
  409. count(idvisit) as `". Piwik_Archive::INDEX_NB_VISITS."`,
  410. case idorder when '0' then ".Piwik_Tracker_GoalManager::IDGOAL_CART." else ".Piwik_Tracker_GoalManager::IDGOAL_ORDER." end as ecommerceType
  411. FROM ".Piwik_Common::prefixTable('log_conversion_item')."
  412. LEFT JOIN ".Piwik_Common::prefixTable('log_action')."
  413. ON $field = idaction
  414. WHERE server_time >= ?
  415. AND server_time <= ?
  416. AND idsite = ?
  417. AND deleted = 0
  418. GROUP BY ecommerceType, $field
  419. ORDER BY NULL";
  420. $bind = array( $this->getStartDatetimeUTC(),
  421. $this->getEndDatetimeUTC(),
  422. $this->idsite
  423. );
  424. $query = $this->db->query($query, $bind);
  425. return $query;
  426. }
  427. static public function getSqlRevenue($field)
  428. {
  429. return "ROUND(".$field.",".Piwik_Tracker_GoalManager::REVENUE_PRECISION.")";
  430. }
  431. public function getDataTableFromArray( $array )
  432. {
  433. $table = new Piwik_DataTable();
  434. $table->addRowsFromArrayWithIndexLabel($array);
  435. return $table;
  436. }
  437. /**
  438. * Output:
  439. * array(
  440. * LABEL => array(
  441. * Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0,
  442. * Piwik_Archive::INDEX_NB_VISITS => 0
  443. * ),
  444. * LABEL2 => array(
  445. * [...]
  446. * )
  447. * )
  448. *
  449. * Helper function that returns an array with common statistics for a given database field distinct values.
  450. *
  451. * The statistics returned are:
  452. * - number of unique visitors
  453. * - number of visits
  454. * - number of actions
  455. * - maximum number of action for a visit
  456. * - sum of the visits' length in sec
  457. * - count of bouncing visits (visits with one page view)
  458. *
  459. * For example if $label = 'config_os' it will return the statistics for every distinct Operating systems
  460. * The returned array will have a row per distinct operating systems,
  461. * and a column per stat (nb of visits, max actions, etc)
  462. *
  463. * 'label' Piwik_Archive::INDEX_NB_UNIQ_VISITORS Piwik_Archive::INDEX_NB_VISITS etc.
  464. * Linux 27 66 ...
  465. * Windows XP 12 ...
  466. * Mac OS 15 36 ...
  467. *
  468. * @param string $label Table log_visit field name to be use to compute common stats
  469. * @return array
  470. */
  471. public function getArrayInterestForLabel($label)
  472. {
  473. $query = $this->queryVisitsByDimension($label);
  474. $interest = array();
  475. while($row = $query->fetch())
  476. {
  477. if(!isset($interest[$row['label']])) $interest[$row['label']]= $this->getNewInterestRow();
  478. $this->updateInterestStats( $row, $interest[$row['label']]);
  479. }
  480. return $interest;
  481. }
  482. /**
  483. * Generates a dataTable given a multidimensional PHP array that associates LABELS to Piwik_DataTableRows
  484. * This is used for the "Actions" DataTable, where a line is the aggregate of all the subtables
  485. * Example: the category /blog has 3 visits because it has /blog/index (2 visits) + /blog/about (1 visit)
  486. *
  487. * @param array $table
  488. * @return Piwik_DataTable
  489. */
  490. static public function generateDataTable( $table, $parents=array() )
  491. {
  492. $dataTableToReturn = new Piwik_DataTable();
  493. foreach($table as $label => $maybeDatatableRow)
  494. {
  495. // case the aInfo is a subtable-like array
  496. // it means that we have to go recursively and process it
  497. // then we build the row that is an aggregate of all the children
  498. // and we associate this row to the subtable
  499. if( !($maybeDatatableRow instanceof Piwik_DataTable_Row) )
  500. {
  501. array_push($parents, array($dataTableToReturn->getId(), $label));
  502. $subTable = self::generateDataTable($maybeDatatableRow, $parents);
  503. $subTable->setParents($parents);
  504. $row = new Piwik_DataTable_Row_DataTableSummary( $subTable );
  505. $row->setColumns( array('label' => $label) + $row->getColumns());
  506. $row->addSubtable($subTable);
  507. array_pop($parents);
  508. }
  509. // if aInfo is a simple Row we build it
  510. else
  511. {
  512. $row = $maybeDatatableRow;
  513. }
  514. $dataTableToReturn->addRow($row);
  515. }
  516. return $dataTableToReturn;
  517. }
  518. /**
  519. * Helper function that returns the serialized DataTable of the given PHP array.
  520. * The array must have the format of Piwik_DataTable::addRowsFromArrayWithIndexLabel()
  521. * Example: array (
  522. * LABEL => array(col1 => X, col2 => Y),
  523. * LABEL2 => array(col1 => X, col2 => Y),
  524. * )
  525. *
  526. * @param array $array at the given format
  527. * @return array Array with one element: the serialized data table string
  528. */
  529. public function getDataTableSerialized( $array )
  530. {
  531. $table = new Piwik_DataTable();
  532. $table->addRowsFromArrayWithIndexLabel($array );
  533. $toReturn = $table->getSerialized();
  534. return $toReturn;
  535. }
  536. /**
  537. * Helper function that returns the multiple serialized DataTable of the given PHP array.
  538. * The DataTable here associates a subtable to every row of the level 0 array.
  539. * This is used for example for search engines.
  540. * Every search engine (level 0) has a subtable containing the keywords.
  541. *
  542. * The $arrayLevel0 must have the format
  543. * Example: array (
  544. * // Yahoo.com => array( kwd1 => stats, kwd2 => stats )
  545. * LABEL => array(col1 => X, col2 => Y),
  546. * LABEL2 => array(col1 => X, col2 => Y),
  547. * )
  548. *
  549. * The $subArrayLevel1ByKey must have the format
  550. * Example: array(
  551. * // Yahoo.com => array( stats )
  552. * LABEL => #Piwik_DataTable_ForLABEL,
  553. * LABEL2 => #Piwik_DataTable_ForLABEL2,
  554. * )
  555. *
  556. *
  557. * @param array $arrayLevel0
  558. * @param array $subArrayLevel1ByKey Array of Piwik_DataTable
  559. * @return array Array with N elements: the strings of the datatable serialized
  560. */
  561. public function getDataTableWithSubtablesFromArraysIndexedByLabel( $arrayLevel0, $subArrayLevel1ByKey )
  562. {
  563. $parentTableLevel0 = new Piwik_DataTable();
  564. $tablesByLabel = array();
  565. foreach($arrayLevel0 as $label => $aAllRowsForThisLabel)
  566. {
  567. $table = new Piwik_DataTable();
  568. $table->addRowsFromArrayWithIndexLabel($aAllRowsForThisLabel);
  569. $tablesByLabel[$label] = $table;
  570. }
  571. $parentTableLevel0->addRowsFromArrayWithIndexLabel($subArrayLevel1ByKey, $tablesByLabel);
  572. return $parentTableLevel0;
  573. }
  574. /**
  575. * Returns an empty row containing default values for the common stat
  576. *
  577. * @return array
  578. */
  579. public function getNewInterestRow($onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false)
  580. {
  581. if($onlyMetricsAvailableInActionsTable)
  582. {
  583. if($doNotSumVisits)
  584. {
  585. return array(Piwik_Archive::INDEX_NB_ACTIONS => 0 );
  586. }
  587. return array(
  588. Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0,
  589. Piwik_Archive::INDEX_NB_VISITS => 0,
  590. Piwik_Archive::INDEX_NB_ACTIONS => 0 );
  591. }
  592. return array( Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0,
  593. Piwik_Archive::INDEX_NB_VISITS => 0,
  594. Piwik_Archive::INDEX_NB_ACTIONS => 0,
  595. Piwik_Archive::INDEX_MAX_ACTIONS => 0,
  596. Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 0,
  597. Piwik_Archive::INDEX_BOUNCE_COUNT => 0,
  598. Piwik_Archive::INDEX_NB_VISITS_CONVERTED=> 0,
  599. );
  600. }
  601. /**
  602. * Returns a Piwik_DataTable_Row containing default values for common stat,
  603. * plus a column 'label' with the value $label
  604. *
  605. * @param string $label
  606. * @return Piwik_DataTable_Row
  607. */
  608. public function getNewInterestRowLabeled( $label )
  609. {
  610. return new Piwik_DataTable_Row(
  611. array(
  612. Piwik_DataTable_Row::COLUMNS => array( 'label' => $label)
  613. + $this->getNewInterestRow()
  614. )
  615. );
  616. }
  617. /**
  618. * Adds the given row $newRowToAdd to the existing $oldRowToUpdate passed by reference
  619. *
  620. * The rows are php arrays Name => value
  621. *
  622. * @param array $newRowToAdd
  623. * @param array $oldRowToUpdate
  624. */
  625. public function updateInterestStats( $newRowToAdd, &$oldRowToUpdate, $onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false)
  626. {
  627. // Pre 1.2 format: string indexed rows are returned from the DB
  628. // Left here for Backward compatibility with plugins doing custom SQL queries using these metrics as string
  629. if(!isset($newRowToAdd[Piwik_Archive::INDEX_NB_VISITS]))
  630. {
  631. if(!$doNotSumVisits)
  632. {
  633. $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors'];
  634. $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd['nb_visits'];
  635. }
  636. $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions'];
  637. if($onlyMetricsAvailableInActionsTable)
  638. {
  639. return;
  640. }
  641. $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd['max_actions'], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]);
  642. $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd['sum_visit_length'];
  643. $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd['bounce_count'];
  644. $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd['nb_visits_converted'];
  645. return;
  646. }
  647. if(!$doNotSumVisits)
  648. {
  649. $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Piwik_Archive::INDEX_NB_UNIQ_VISITORS];
  650. $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS];
  651. }
  652. $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd[Piwik_Archive::INDEX_NB_ACTIONS];
  653. // Hack for Price tracking on Ecommerce product/category pages
  654. // The price is not summed, but AVG is taken in the SQL query
  655. $index = Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED;
  656. if(!empty($newRowToAdd[$index]))
  657. {
  658. $oldRowToUpdate[$index] = (float)$newRowToAdd[$index];
  659. }
  660. if($onlyMetricsAvailableInActionsTable)
  661. {
  662. return;
  663. }
  664. $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd[Piwik_Archive::INDEX_MAX_ACTIONS], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]);
  665. $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd[Piwik_Archive::INDEX_SUM_VISIT_LENGTH];
  666. $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd[Piwik_Archive::INDEX_BOUNCE_COUNT];
  667. $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS_CONVERTED];
  668. }
  669. /**
  670. * Given an array of stats, it will process the sum of goal conversions
  671. * and sum of revenue and add it in the stats array in two new fields.
  672. *
  673. * @param array $interestByLabel Passed by reference, it will be modified as follows:
  674. * Input:
  675. * array(
  676. * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X,
  677. * Piwik_Archive::INDEX_GOALS => array(
  678. * idgoal1 => array( [...] ),
  679. * idgoal2 => array( [...] ),
  680. * ),
  681. * [...] ),
  682. * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] )
  683. * );
  684. *
  685. *
  686. * Output:
  687. * array(
  688. * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X,
  689. * Piwik_Archive::INDEX_NB_CONVERSIONS => Y, // sum of all conversions
  690. * Piwik_Archive::INDEX_REVENUE => Z, // sum of all revenue
  691. * Piwik_Archive::INDEX_GOALS => array(
  692. * idgoal1 => array( [...] ),
  693. * idgoal2 => array( [...] ),
  694. * ),
  695. * [...] ),
  696. * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] )
  697. * );
  698. * )
  699. *
  700. * @param array $interestByLabel Passed by reference, will be modified
  701. */
  702. function enrichConversionsByLabelArray(&$interestByLabel)
  703. {
  704. foreach($interestByLabel as $label => &$values)
  705. {
  706. if(isset($values[Piwik_Archive::INDEX_GOALS]))
  707. {
  708. // When per goal metrics are processed, general 'visits converted' is not meaningful because
  709. // it could differ from the sum of each goal conversions
  710. unset($values[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]);
  711. $revenue = $conversions = 0;
  712. foreach($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues)
  713. {
  714. // Do not sum Cart revenue since it is a lost revenue
  715. if($idgoal >= Piwik_Tracker_GoalManager::IDGOAL_ORDER)
  716. {
  717. $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE];
  718. $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS];
  719. }
  720. }
  721. $values[Piwik_Archive::INDEX_NB_CONVERSIONS] = $conversions;
  722. // 25.00 recorded as 25
  723. if(round($revenue) == $revenue)
  724. {
  725. $revenue = round($revenue);
  726. }
  727. $values[Piwik_Archive::INDEX_REVENUE] = $revenue;
  728. }
  729. }
  730. }
  731. /**
  732. * @param array $interestByLabelAndSubLabel Passed by reference, will be modified
  733. */
  734. function enrichConversionsByLabelArrayHasTwoLevels(&$interestByLabelAndSubLabel)
  735. {
  736. foreach($interestByLabelAndSubLabel as $mainLabel => &$interestBySubLabel)
  737. {
  738. $this->enrichConversionsByLabelArray($interestBySubLabel);
  739. }
  740. }
  741. function updateGoalStats($newRowToAdd, &$oldRowToUpdate)
  742. {
  743. $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS];
  744. $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED];
  745. $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_REVENUE] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_REVENUE];
  746. // Cart & Order
  747. if(isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS]))
  748. {
  749. $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS];
  750. // Order only
  751. if(isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL]))
  752. {
  753. $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL];
  754. $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX];
  755. $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING];
  756. $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT];
  757. }
  758. }
  759. }
  760. function getNewGoalRow($idGoal)
  761. {
  762. if($idGoal > Piwik_Tracker_GoalManager::IDGOAL_ORDER)
  763. {
  764. return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0,
  765. Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0,
  766. Piwik_Archive::INDEX_GOAL_REVENUE => 0,
  767. );
  768. }
  769. if($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER)
  770. {
  771. return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0,
  772. Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0,
  773. Piwik_Archive::INDEX_GOAL_REVENUE => 0,
  774. Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 0,
  775. Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 0,
  776. Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 0,
  777. Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 0,
  778. Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0,
  779. );
  780. }
  781. // $row['idgoal'] == Piwik_Tracker_GoalManager::IDGOAL_CART
  782. return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0,
  783. Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0,
  784. Piwik_Archive::INDEX_GOAL_REVENUE => 0,
  785. Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0,
  786. );
  787. }
  788. }