PageRenderTime 48ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/Goals/API.php

https://github.com/quarkness/piwik
PHP | 567 lines | 360 code | 47 blank | 160 comment | 40 complexity | 86d0d0d9d40623cecd49c96c86a8925c 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_Plugins
  10. * @package Piwik_Goals
  11. */
  12. /**
  13. * Goals API lets you Manage existing goals, via "updateGoal" and "deleteGoal", create new Goals via "addGoal",
  14. * or list existing Goals for one or several websites via "getGoals"
  15. *
  16. * If you are <a href='http://piwik.org/docs/ecommerce-analytics/' target='_blank'>tracking Ecommerce orders and products</a> on your site, the functions "getItemsSku", "getItemsName" and "getItemsCategory"
  17. * will return the list of products purchased on your site, either grouped by Product SKU, Product Name or Product Category. For each name, SKU or category, the following
  18. * metrics are returned: Total revenue, Total quantity, average price, average quantity, number of orders (or abandoned carts) containing this product, number of visits on the Product page,
  19. * Conversion rate.
  20. *
  21. * By default, these functions return the 'Products purchased'. These functions also accept an optional parameter &abandonedCarts=1.
  22. * If the parameter is set, it will instead return the metrics for products that were left in an abandoned cart therefore not purchased.
  23. *
  24. * The API also lets you request overall Goal metrics via the method "get": Conversions, Visits with at least one conversion, Conversion rate and Revenue.
  25. * If you wish to request specific metrics about Ecommerce goals, you can set the parameter &idGoal=ecommerceAbandonedCart to get metrics about abandoned carts (including Lost revenue, and number of items left in the cart)
  26. * or &idGoal=ecommerceOrder to get metrics about Ecommerce orders (number of orders, visits with an order, subtotal, tax, shipping, discount, revenue, items ordered)
  27. *
  28. * See also the documentation about <a href='http://piwik.org/docs/tracking-goals-web-analytics/' target='_blank'>Tracking Goals</a> in Piwik.
  29. *
  30. * @package Piwik_Goals
  31. */
  32. class Piwik_Goals_API
  33. {
  34. static private $instance = null;
  35. /**
  36. * @return Piwik_Goals_API
  37. */
  38. static public function getInstance()
  39. {
  40. if (self::$instance == null)
  41. {
  42. self::$instance = new self;
  43. }
  44. return self::$instance;
  45. }
  46. /**
  47. * Returns all Goals for a given website, or list of websites
  48. *
  49. * @param string|array $idSite Array or Comma separated list of website IDs to request the goals for
  50. * @return array Array of Goal attributes
  51. */
  52. public function getGoals( $idSite )
  53. {
  54. //TODO calls to this function could be cached as static
  55. // would help UI at least, since some UI requests would call this 2-3 times..
  56. if(!is_array($idSite))
  57. {
  58. $idSite = Piwik_Site::getIdSitesFromIdSitesString($idSite);
  59. }
  60. if(empty($idSite))
  61. {
  62. return array();
  63. }
  64. Piwik::checkUserHasViewAccess($idSite);
  65. $goals = Piwik_FetchAll("SELECT *
  66. FROM ".Piwik_Common::prefixTable('goal')."
  67. WHERE idsite IN (".implode(", ", $idSite).")
  68. AND deleted = 0");
  69. $cleanedGoals = array();
  70. foreach($goals as &$goal)
  71. {
  72. if($goal['match_attribute'] == 'manually') {
  73. unset($goal['pattern']);
  74. unset($goal['pattern_type']);
  75. unset($goal['case_sensitive']);
  76. }
  77. $cleanedGoals[$goal['idgoal']] = $goal;
  78. }
  79. return $cleanedGoals;
  80. }
  81. /**
  82. * Creates a Goal for a given website.
  83. *
  84. * @param int $idSite
  85. * @param string $name
  86. * @param string $matchAttribute 'url', 'title', 'file', 'external_website' or 'manually'
  87. * @param string $pattern eg. purchase-confirmation.htm
  88. * @param string $patternType 'regex', 'contains', 'exact'
  89. * @param bool $caseSensitive
  90. * @param float|string $revenue If set, default revenue to assign to conversions
  91. * @param bool $allowMultipleConversionsPerVisit By default, multiple conversions in the same visit will only record the first conversion.
  92. * If set to true, multiple conversions will all be recorded within a visit (useful for Ecommerce goals)
  93. * @return int ID of the new goal
  94. */
  95. public function addGoal( $idSite, $name, $matchAttribute, $pattern, $patternType, $caseSensitive = false, $revenue = false, $allowMultipleConversionsPerVisit = false)
  96. {
  97. Piwik::checkUserHasAdminAccess($idSite);
  98. $this->checkPatternIsValid($patternType, $pattern);
  99. $name = $this->checkName($name);
  100. $pattern = $this->checkPattern($pattern);
  101. // save in db
  102. $db = Zend_Registry::get('db');
  103. $idGoal = $db->fetchOne("SELECT max(idgoal) + 1
  104. FROM ".Piwik_Common::prefixTable('goal')."
  105. WHERE idsite = ?", $idSite);
  106. if($idGoal == false)
  107. {
  108. $idGoal = 1;
  109. }
  110. $db->insert(Piwik_Common::prefixTable('goal'),
  111. array(
  112. 'idsite' => $idSite,
  113. 'idgoal' => $idGoal,
  114. 'name' => $name,
  115. 'match_attribute' => $matchAttribute,
  116. 'pattern' => $pattern,
  117. 'pattern_type' => $patternType,
  118. 'case_sensitive' => (int)$caseSensitive,
  119. 'allow_multiple' => (int)$allowMultipleConversionsPerVisit,
  120. 'revenue' => (float)$revenue,
  121. 'deleted' => 0,
  122. ));
  123. Piwik_Common::regenerateCacheWebsiteAttributes($idSite);
  124. return $idGoal;
  125. }
  126. /**
  127. * Updates a Goal description.
  128. * Will not update or re-process the conversions already recorded
  129. *
  130. * @see addGoal() for parameters description
  131. * @return void
  132. */
  133. public function updateGoal( $idSite, $idGoal, $name, $matchAttribute, $pattern, $patternType, $caseSensitive = false, $revenue = false, $allowMultipleConversionsPerVisit = false)
  134. {
  135. Piwik::checkUserHasAdminAccess($idSite);
  136. $name = $this->checkName($name);
  137. $pattern = $this->checkPattern($pattern);
  138. $this->checkPatternIsValid($patternType, $pattern);
  139. Zend_Registry::get('db')->update( Piwik_Common::prefixTable('goal'),
  140. array(
  141. 'name' => $name,
  142. 'match_attribute' => $matchAttribute,
  143. 'pattern' => $pattern,
  144. 'pattern_type' => $patternType,
  145. 'case_sensitive' => (int)$caseSensitive,
  146. 'allow_multiple' => (int)$allowMultipleConversionsPerVisit,
  147. 'revenue' => (float)$revenue,
  148. ),
  149. "idsite = '$idSite' AND idgoal = '$idGoal'"
  150. );
  151. Piwik_Common::regenerateCacheWebsiteAttributes($idSite);
  152. }
  153. private function checkPatternIsValid($patternType, $pattern)
  154. {
  155. if($patternType == 'exact'
  156. && substr($pattern, 0, 4) != 'http')
  157. {
  158. throw new Exception(Piwik_TranslateException('Goals_ExceptionInvalidMatchingString', array("http:// or https://", "http://www.yourwebsite.com/newsletter/subscribed.html")));
  159. }
  160. }
  161. private function checkName($name)
  162. {
  163. return urldecode($name);
  164. }
  165. private function checkPattern($pattern)
  166. {
  167. return urldecode($pattern);
  168. }
  169. /**
  170. * Soft deletes a given Goal.
  171. * Stats data in the archives will still be recorded, but not displayed.
  172. *
  173. * @param int $idSite
  174. * @param int $idGoal
  175. * @return void
  176. */
  177. public function deleteGoal( $idSite, $idGoal )
  178. {
  179. Piwik::checkUserHasAdminAccess($idSite);
  180. Piwik_Query("UPDATE ".Piwik_Common::prefixTable('goal')."
  181. SET deleted = 1
  182. WHERE idsite = ?
  183. AND idgoal = ?",
  184. array($idSite, $idGoal));
  185. Piwik_Query("DELETE FROM ".Piwik_Common::prefixTable("log_conversion")." WHERE idgoal = ?", $idGoal);
  186. Piwik_Common::regenerateCacheWebsiteAttributes($idSite);
  187. }
  188. /**
  189. * Returns a datatable of Items SKU/name or categories and their metrics
  190. * If $abandonedCarts set to 1, will return items abandoned in carts. If set to 0, will return items ordered
  191. */
  192. protected function getItems($recordName, $idSite, $period, $date, $abandonedCarts )
  193. {
  194. Piwik::checkUserHasViewAccess( $idSite );
  195. $recordNameFinal = $recordName;
  196. if($abandonedCarts)
  197. {
  198. $recordNameFinal = Piwik_Goals::getItemRecordNameAbandonedCart($recordName);
  199. }
  200. $archive = Piwik_Archive::build($idSite, $period, $date );
  201. $dataTable = $archive->getDataTable($recordNameFinal);
  202. $dataTable->filter('Sort', array(Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE));
  203. $dataTable->queueFilter('ReplaceColumnNames');
  204. $ordersColumn = 'orders';
  205. if($abandonedCarts)
  206. {
  207. $ordersColumn = 'abandoned_carts';
  208. $dataTable->renameColumn(Piwik_Archive::INDEX_ECOMMERCE_ORDERS, $ordersColumn);
  209. }
  210. // Average price = sum product revenue / quantity
  211. $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_price', 'price', $ordersColumn, Piwik_Tracker_GoalManager::REVENUE_PRECISION));
  212. // Average quantity = sum product quantity / abandoned carts
  213. $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_quantity', 'quantity', $ordersColumn, $precision = 1));
  214. $dataTable->queueFilter('ColumnDelete', array('price'));
  215. // Enrich the datatable with Product/Categories views, and conversion rates
  216. $customVariables = Piwik_CustomVariables_API::getInstance()->getCustomVariables($idSite, $period, $date, $segment = false, $expanded = false, $_leavePiwikCoreVariables = true);
  217. $mapping = array(
  218. 'Goals_ItemsSku' => '_pks',
  219. 'Goals_ItemsName' => '_pkn',
  220. 'Goals_ItemsCategory' => '_pkc',
  221. );
  222. $reportToNotDefinedString = array(
  223. 'Goals_ItemsSku' => Piwik_Translate('General_NotDefined', Piwik_Translate('Goals_ProductSKU')), // Note: this should never happen
  224. 'Goals_ItemsName' => Piwik_Translate('General_NotDefined', Piwik_Translate('Goals_ProductName')),
  225. 'Goals_ItemsCategory' => Piwik_Translate('General_NotDefined', Piwik_Translate('Goals_ProductCategory'))
  226. );
  227. $notDefinedStringPretty = $reportToNotDefinedString[$recordName];
  228. // Handle case where date=last30&period=day
  229. if($customVariables instanceof Piwik_DataTable_Array)
  230. {
  231. $customVariableDatatables = $customVariables->getArray();
  232. $dataTables = $dataTable->getArray();
  233. foreach($customVariableDatatables as $key => $customVariableTableForDate)
  234. {
  235. $dataTableForDate = isset($dataTables[$key]) ? $dataTables[$key] : new Piwik_DataTable();
  236. // we do not enter the IF
  237. // if case idSite=1,3 AND period=day&date=datefrom,dateto,
  238. if(isset($dataTable->metadata[$key]['period']))
  239. {
  240. $dateRewrite = $dataTable->metadata[$key]['period']->getDateStart()->toString();
  241. $row = $customVariableTableForDate->getRowFromLabel($mapping[$recordName]);
  242. if($row)
  243. {
  244. $idSubtable = $row->getIdSubDataTable();
  245. $this->enrichItemsDataTableWithItemsViewMetrics($dataTableForDate, $idSite, $period, $dateRewrite, $idSubtable);
  246. }
  247. $dataTable->addTable($dataTableForDate, $key);
  248. }
  249. $this->renameNotDefinedRow($dataTableForDate, $notDefinedStringPretty);
  250. }
  251. }
  252. elseif($customVariables instanceof Piwik_DataTable)
  253. {
  254. $row = $customVariables->getRowFromLabel($mapping[$recordName]);
  255. if($row)
  256. {
  257. $idSubtable = $row->getIdSubDataTable();
  258. $this->enrichItemsDataTableWithItemsViewMetrics($dataTable, $idSite, $period, $date, $idSubtable);
  259. }
  260. $this->renameNotDefinedRow($dataTable, $notDefinedStringPretty);
  261. }
  262. // Product conversion rate = orders / visits
  263. $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('conversion_rate', $ordersColumn, 'nb_visits', Piwik_Tracker_GoalManager::REVENUE_PRECISION));
  264. return $dataTable;
  265. }
  266. protected function renameNotDefinedRow($dataTable, $notDefinedStringPretty)
  267. {
  268. if($dataTable instanceof Piwik_DataTable_Array)
  269. {
  270. foreach($dataTable->getArray() as $table)
  271. {
  272. $this->renameNotDefinedRow($table, $notDefinedStringPretty);
  273. }
  274. return;
  275. }
  276. $rowNotDefined = $dataTable->getRowFromLabel(Piwik_CustomVariables::LABEL_CUSTOM_VALUE_NOT_DEFINED);
  277. if($rowNotDefined)
  278. {
  279. $rowNotDefined->setColumn('label', $notDefinedStringPretty);
  280. }
  281. }
  282. protected function enrichItemsDataTableWithItemsViewMetrics($dataTable, $idSite, $period, $date, $idSubtable)
  283. {
  284. $ecommerceViews = Piwik_CustomVariables_API::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $date, $idSubtable, $segment = false, $_leavePriceViewedColumn = true);
  285. // For Product names and SKU reports, and for Category report
  286. // Use the Price (tracked on page views)
  287. // ONLY when the price sold in conversions is not found (ie. product viewed but not sold)
  288. foreach($ecommerceViews->getRows() as $rowView)
  289. {
  290. // If there is not already a 'sum price' for this product
  291. $rowFound = $dataTable->getRowFromLabel($rowView->getColumn('label'));
  292. $price = $rowFound
  293. ? $rowFound->getColumn(Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE)
  294. : false;
  295. if(empty($price))
  296. {
  297. // If a price was tracked on the product page
  298. if($rowView->getColumn(Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED))
  299. {
  300. $rowView->renameColumn(Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED, 'avg_price');
  301. }
  302. }
  303. $rowView->deleteColumn(Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED);
  304. }
  305. $dataTable->addDataTable($ecommerceViews);
  306. }
  307. public function getItemsSku($idSite, $period, $date, $abandonedCarts = false )
  308. {
  309. return $this->getItems('Goals_ItemsSku', $idSite, $period, $date, $abandonedCarts);
  310. }
  311. public function getItemsName($idSite, $period, $date, $abandonedCarts = false )
  312. {
  313. return $this->getItems('Goals_ItemsName', $idSite, $period, $date, $abandonedCarts);
  314. }
  315. public function getItemsCategory($idSite, $period, $date, $abandonedCarts = false )
  316. {
  317. return $this->getItems('Goals_ItemsCategory', $idSite, $period, $date, $abandonedCarts);
  318. }
  319. /**
  320. * Helper function that checks for special string goal IDs and converts them to
  321. * their integer equivalents.
  322. *
  323. * Checks for the following values:
  324. * Piwik_Archive::LABEL_ECOMMERCE_ORDER
  325. * Piwik_Archive::LABEL_ECOMMERCE_CART
  326. *
  327. * @param string|int $idGoal The goal id as an integer or a special string.
  328. * @return int The numeric goal id.
  329. */
  330. protected static function convertSpecialGoalIds( $idGoal )
  331. {
  332. if ($idGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER)
  333. {
  334. return Piwik_Tracker_GoalManager::IDGOAL_ORDER;
  335. }
  336. else if ($idGoal == Piwik_Archive::LABEL_ECOMMERCE_CART)
  337. {
  338. return Piwik_Tracker_GoalManager::IDGOAL_CART;
  339. }
  340. else
  341. {
  342. return $idGoal;
  343. }
  344. }
  345. /**
  346. * Returns Goals data
  347. *
  348. * @param int $idSite
  349. * @param string $period
  350. * @param string $date
  351. * @param int $idGoal
  352. * @param array $columns Array of metrics to fetch: nb_conversions, conversion_rate, revenue
  353. * @return Piwik_DataTable
  354. */
  355. public function get( $idSite, $period, $date, $segment = false, $idGoal = false, $columns = array() )
  356. {
  357. Piwik::checkUserHasViewAccess( $idSite );
  358. $archive = Piwik_Archive::build($idSite, $period, $date, $segment );
  359. $columns = Piwik::getArrayFromApiParameter($columns);
  360. // Mapping string idGoal to internal ID
  361. $idGoal = self::convertSpecialGoalIds($idGoal);
  362. if(empty($columns))
  363. {
  364. $columns = Piwik_Goals::getGoalColumns($idGoal);
  365. if($idGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER)
  366. {
  367. $columns[] = 'avg_order_revenue';
  368. }
  369. }
  370. if(in_array('avg_order_revenue', $columns)
  371. && $idGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER)
  372. {
  373. $columns[] = 'nb_conversions';
  374. $columns[] = 'revenue';
  375. $columns = array_values(array_unique($columns));
  376. }
  377. $columnsToSelect = array();
  378. foreach($columns as &$columnName)
  379. {
  380. $columnsToSelect[] = Piwik_Goals::getRecordName($columnName, $idGoal);
  381. }
  382. $dataTable = $archive->getDataTableFromNumeric($columnsToSelect);
  383. // Rewrite column names as we expect them
  384. foreach($columnsToSelect as $id => $oldName)
  385. {
  386. $dataTable->renameColumn($oldName, $columns[$id]);
  387. }
  388. if($idGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER)
  389. {
  390. if($dataTable instanceof Piwik_DataTable_Array)
  391. {
  392. foreach($dataTable->getArray() as $row)
  393. {
  394. $this->enrichTable($row);
  395. }
  396. }
  397. else
  398. {
  399. $this->enrichTable($dataTable);
  400. }
  401. }
  402. return $dataTable;
  403. }
  404. protected function enrichTable($table)
  405. {
  406. $row = $table->getFirstRow();
  407. if(!$row)
  408. {
  409. return;
  410. }
  411. // AVG order per visit
  412. if(false !== $table->getColumn('avg_order_revenue'))
  413. {
  414. $conversions = $row->getColumn('nb_conversions');
  415. if($conversions)
  416. {
  417. $row->setColumn('avg_order_revenue', round($row->getColumn('revenue') / $conversions, 2));
  418. }
  419. }
  420. }
  421. protected function getNumeric( $idSite, $period, $date, $segment, $toFetch )
  422. {
  423. Piwik::checkUserHasViewAccess( $idSite );
  424. $archive = Piwik_Archive::build($idSite, $period, $date, $segment );
  425. $dataTable = $archive->getNumeric($toFetch);
  426. return $dataTable;
  427. }
  428. /**
  429. * @ignore
  430. */
  431. public function getConversions( $idSite, $period, $date, $segment = false, $idGoal = false )
  432. {
  433. return $this->getNumeric( $idSite, $period, $date, $segment, Piwik_Goals::getRecordName('nb_conversions', $idGoal));
  434. }
  435. /**
  436. * @ignore
  437. */
  438. public function getNbVisitsConverted( $idSite, $period, $date, $segment = false, $idGoal = false )
  439. {
  440. return $this->getNumeric( $idSite, $period, $date, $segment, Piwik_Goals::getRecordName('nb_visits_converted', $idGoal));
  441. }
  442. /**
  443. * @ignore
  444. */
  445. public function getConversionRate( $idSite, $period, $date, $segment = false, $idGoal = false )
  446. {
  447. return $this->getNumeric( $idSite, $period, $date, $segment, Piwik_Goals::getRecordName('conversion_rate', $idGoal));
  448. }
  449. /**
  450. * @ignore
  451. */
  452. public function getRevenue( $idSite, $period, $date, $segment = false, $idGoal = false )
  453. {
  454. return $this->getNumeric( $idSite, $period, $date, $segment, Piwik_Goals::getRecordName('revenue', $idGoal));
  455. }
  456. /**
  457. * Utility method that retrieve an archived DataTable for a specific site, date range,
  458. * segment and goal. If not goal is specified, this method will retrieve and sum the
  459. * data for every goal.
  460. *
  461. * @param string $recordName The archive entry name.
  462. * @param int|string $idSite The site(s) to select data for.
  463. * @param string $period The period type.
  464. * @param string $date The date type.
  465. * @param string $segment The segment.
  466. * @param int|bool $idGoal The id of the goal to get data for. If this is set to false,
  467. * data for every goal that belongs to $idSite is returned.
  468. */
  469. protected function getGoalSpecificDataTable($recordName, $idSite, $period, $date, $segment, $idGoal)
  470. {
  471. Piwik::checkUserHasViewAccess( $idSite );
  472. $archive = Piwik_Archive::build($idSite, $period, $date, $segment );
  473. // check for the special goal ids
  474. $realGoalId = $idGoal != true ? false : self::convertSpecialGoalIds($idGoal);
  475. // get the data table
  476. $dataTable = $archive->getDataTable(Piwik_Goals::getRecordName($recordName, $realGoalId), $idSubtable = null);
  477. $dataTable->queueFilter('ReplaceColumnNames');
  478. return $dataTable;
  479. }
  480. /**
  481. * Gets a DataTable that maps ranges of days to the number of conversions that occurred
  482. * within those ranges, for the specified site, date range, segment and goal.
  483. *
  484. * @param int $idSite The site to select data from.
  485. * @param string $period The period type.
  486. * @param string $date The date type.
  487. * @param string|bool $segment The segment.
  488. * @param int|bool $idGoal The id of the goal to get data for. If this is set to false,
  489. * data for every goal that belongs to $idSite is returned.
  490. */
  491. public function getDaysToConversion($idSite, $period, $date, $segment = false, $idGoal = false)
  492. {
  493. $dataTable = $this->getGoalSpecificDataTable(
  494. Piwik_Goals::DAYS_UNTIL_CONV_RECORD_NAME, $idSite, $period, $date, $segment, $idGoal);
  495. $dataTable->queueFilter('Sort', array('label', 'asc', true));
  496. $dataTable->queueFilter(
  497. 'BeautifyRangeLabels', array(Piwik_Translate('General_OneDay'), Piwik_Translate('General_NDays')));
  498. return $dataTable;
  499. }
  500. /**
  501. * Gets a DataTable that maps ranges of visit counts to the number of conversions that
  502. * occurred on those visits for the specified site, date range, segment and goal.
  503. *
  504. * @param int $idSite The site to select data from.
  505. * @param string $period The period type.
  506. * @param string $date The date type.
  507. * @param string|bool $segment The segment.
  508. * @param int|bool $idGoal The id of the goal to get data for. If this is set to false,
  509. * data for every goal that belongs to $idSite is returned.
  510. */
  511. public function getVisitsUntilConversion($idSite, $period, $date, $segment = false, $idGoal = false)
  512. {
  513. $dataTable = $this->getGoalSpecificDataTable(
  514. Piwik_Goals::VISITS_UNTIL_RECORD_NAME, $idSite, $period, $date, $segment, $idGoal);
  515. $dataTable->queueFilter('Sort', array('label', 'asc', true));
  516. $dataTable->queueFilter(
  517. 'BeautifyRangeLabels', array(Piwik_Translate('General_OneVisit'), Piwik_Translate('General_NVisits')));
  518. return $dataTable;
  519. }
  520. }