diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php index 074783bec135c30e3ac9d1dbfe1e4b295483d3de..6b087b8f035dabf46b98dcd5859bc521225ca78d 100644 --- a/core/API/DocumentationGenerator.php +++ b/core/API/DocumentationGenerator.php @@ -135,7 +135,8 @@ class Piwik_API_DocumentationGenerator 'url' => 'http://forum.piwik.org/', 'apiModule' => 'UserCountry', 'apiAction' => 'getCountry', - 'lastMinutes' => '30' + 'lastMinutes' => '30', + 'abandonedCarts' => '0', ); foreach($parametersToSet as $name => $value) diff --git a/core/API/Proxy.php b/core/API/Proxy.php index 61e3a2da163b9c426d1c7b307dd7ff3c4c37bf5d..1a283e221e8e95bb24f0d6d2447c118d884efbd9 100644 --- a/core/API/Proxy.php +++ b/core/API/Proxy.php @@ -35,6 +35,7 @@ class Piwik_API_Proxy protected $alreadyRegistered = array(); private $metadataArray = array(); + public $hideIgnoredFunctions = true; // when a parameter doesn't have a default value we use this private $noDefaultValue; @@ -295,7 +296,7 @@ class Piwik_API_Proxy && !$method->isConstructor() && $method->getName() != 'getInstance' && false === strstr($method->getDocComment(), '@deprecated') - && false === strstr($method->getDocComment(), '@ignore') + && (!$this->hideIgnoredFunctions || false === strstr($method->getDocComment(), '@ignore')) ) { $name = $method->getName(); diff --git a/core/Archive.php b/core/Archive.php index 65c26ae03ae6b4e4ed7f5e55cddcf81862c80fdb..4a70e43eee6c74f0af798be0bb030eda564d0639 100644 --- a/core/Archive.php +++ b/core/Archive.php @@ -68,11 +68,23 @@ abstract class Piwik_Archive const INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH = 21; const INDEX_PAGE_ENTRY_BOUNCE_COUNT = 22; + // Ecommerce Items reports + const INDEX_ECOMMERCE_ITEM_REVENUE = 23; + const INDEX_ECOMMERCE_ITEM_QUANTITY = 24; + const INDEX_ECOMMERCE_ITEM_PRICE = 25; + const INDEX_ECOMMERCE_ORDERS = 26; + // Goal reports const INDEX_GOAL_NB_CONVERSIONS = 1; const INDEX_GOAL_REVENUE = 2; const INDEX_GOAL_NB_VISITS_CONVERTED = 3; - + + const INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL = 4; + const INDEX_GOAL_ECOMMERCE_REVENUE_TAX = 5; + const INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING = 6; + const INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT = 7; + const INDEX_GOAL_ECOMMERCE_ITEMS = 8; + public static $mappingFromIdToName = array( Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 'nb_uniq_visitors', Piwik_Archive::INDEX_NB_VISITS => 'nb_visits', @@ -100,12 +112,23 @@ abstract class Piwik_Archive Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS => 'entry_nb_actions', Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH => 'entry_sum_visit_length', Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT => 'entry_bounce_count', + + // Items reports metrics + Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE => 'revenue', + Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY => 'quantity', + Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE => 'price', + Piwik_Archive::INDEX_ECOMMERCE_ORDERS => 'orders', ); public static $mappingFromIdToNameGoal = array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 'nb_conversions', Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 'nb_visits_converted', Piwik_Archive::INDEX_GOAL_REVENUE => 'revenue', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 'revenue_subtotal', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 'revenue_tax', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 'revenue_shipping', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 'revenue_discount', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 'items', ); /* @@ -125,6 +148,9 @@ abstract class Piwik_Archive 'sum_daily_nb_uniq_visitors' => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, ); + const LABEL_ECOMMERCE_CART = 'ecommerceAbandonedCart'; + const LABEL_ECOMMERCE_ORDER = 'ecommerceOrder'; + /** * Website Piwik_Site * diff --git a/core/ArchiveProcessing/Day.php b/core/ArchiveProcessing/Day.php index cb00e5cc4f4b8f11402bf8d7312584a6c9eb225b..37b4b187ee34be9ac4760e6aa2a6118702b004c4 100644 --- a/core/ArchiveProcessing/Day.php +++ b/core/ArchiveProcessing/Day.php @@ -240,7 +240,9 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing 'referer_keyword', 'visitor_returning', 'visitor_days_since_first', + 'visitor_days_since_order', 'visitor_count_visits', + 'visit_goal_buyer', 'location_country', 'location_continent', 'revenue', @@ -267,18 +269,25 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing } /** - * @see queryVisitsByDimension() Similar to this function, but queries metrics for the requested dimensions, for each Goal conversion + * @see queryVisitsByDimension() Similar to this function, + * but queries metrics for the requested dimensions, + * for each Goal conversion */ public function queryConversionsByDimension($label, $where = '') { - if(is_array($label)) + if(empty($label)) + { + $select = ""; + $groupBy = ""; + } + elseif(is_array($label)) { - $select = implode(", ", $label); - $groupBy = $select; + $groupBy = implode(", ", $label); + $select = $groupBy . ", "; } else { - $select = $label . " AS label "; + $select = $label . " AS label, "; $groupBy = 'label'; } if(!empty($where)) @@ -295,20 +304,32 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing { $segment = ' AND '.$segmentSql['sql']; } - $query = "SELECT idgoal, - count(*) as `". Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS ."`, - truncate(sum(revenue),2) as `". Piwik_Archive::INDEX_GOAL_REVENUE ."`, - count(distinct idvisit) as `". Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED."`, + + $isEcommerceEnabled = Piwik::isEcommerceEnabled($this->idsite); + + if($isEcommerceEnabled) + { + $select .= $this->getSqlRevenue('SUM(revenue_subtotal)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL ."`,". + $this->getSqlRevenue('SUM(revenue_tax)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX ."`,". + $this->getSqlRevenue('SUM(revenue_shipping)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING ."`,". + $this->getSqlRevenue('SUM(revenue_discount)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT ."`,". + "SUM(items) as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS ."`, "; + } + $groupBy = !empty($groupBy) ? ", $groupBy" : ''; + $query = "SELECT $select + idgoal, + count(*) as `". Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS ."`, + ".$this->getSqlRevenue('SUM(revenue)')." as `". Piwik_Archive::INDEX_GOAL_REVENUE ."`, + count(distinct idvisit) as `". Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED."` FROM ".Piwik_Common::prefixTable('log_conversion')." WHERE server_time >= ? AND server_time <= ? AND idsite = ? $where $segment - GROUP BY idgoal, $groupBy + GROUP BY idgoal $groupBy ORDER BY NULL"; - $bind = array_merge( array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ), @@ -317,6 +338,38 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing return $query; } + public function queryEcommerceItems($field) + { + $query = "SELECT + name as label, + ".$this->getSqlRevenue('SUM(quantity * price)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE ."`, + ".$this->getSqlRevenue('SUM(quantity)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY ."`, + ".$this->getSqlRevenue('SUM(price)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE ."`, + count(distinct idorder) as `". Piwik_Archive::INDEX_ECOMMERCE_ORDERS."`, + count(idvisit) as `". Piwik_Archive::INDEX_NB_VISITS."`, + case idorder when 0 then ".Piwik_Tracker_GoalManager::IDGOAL_CART." else ".Piwik_Tracker_GoalManager::IDGOAL_ORDER." end as type + FROM ".Piwik_Common::prefixTable('log_conversion_item')." + LEFT JOIN ".Piwik_Common::prefixTable('log_action')." + ON $field = idaction + WHERE server_time >= ? + AND server_time <= ? + AND idsite = ? + AND deleted = 0 + GROUP BY $field, type + ORDER BY NULL"; + + $bind = array( $this->getStartDatetimeUTC(), + $this->getEndDatetimeUTC(), + $this->idsite + ); + $query = $this->db->query($query, $bind); + return $query; + } + + protected function getSqlRevenue($field) + { + return "ROUND(".$field.",".Piwik_Tracker_GoalManager::REVENUE_PRECISION.")"; + } public function getDataTableFromArray( $array ) { @@ -580,8 +633,12 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing $revenue = $conversions = 0; foreach($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues) { - $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE]; - $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; + // 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; $values[Piwik_Archive::INDEX_REVENUE] = $revenue; @@ -605,14 +662,50 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing $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]; + } + } } - function getNewGoalRow() + function getNewGoalRow($idGoal) { + 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, + ); + } + // $row['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, + ); } function getGoalRowFromQueryRow($queryRow) diff --git a/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php b/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php index eb4359437e55d0183036bcba54cd8471af0ff4fd..50a1e665b2c00c1418f581245169084b1ef53b6a 100644 --- a/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php +++ b/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php @@ -59,7 +59,7 @@ class Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal extends Piwik_DataTa { // Add standard processed metrics parent::filter($table); - $roundingPrecision = 2; + $roundingPrecision = Piwik_Tracker_GoalManager::REVENUE_PRECISION; $expectedColumns = array(); foreach($table->getRows() as $key => $row) @@ -76,7 +76,10 @@ class Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal extends Piwik_DataTa $revenue = 0; foreach($goals as $goalId => $columnValue) { - $revenue += (int)$this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_REVENUE, Piwik_Archive::$mappingFromIdToNameGoal); + if($goalId >= Piwik_Tracker_GoalManager::IDGOAL_ORDER) + { + $revenue += (int)$this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_REVENUE, Piwik_Archive::$mappingFromIdToNameGoal); + } } if($revenue == 0) diff --git a/core/DataTable/Filter/ReplaceColumnNames.php b/core/DataTable/Filter/ReplaceColumnNames.php index 2d22de7046b69853296feae7631df556116dc347..810ba5c3222893201b1a7f9b7bd9d85eb6b117fe 100644 --- a/core/DataTable/Filter/ReplaceColumnNames.php +++ b/core/DataTable/Filter/ReplaceColumnNames.php @@ -71,9 +71,18 @@ class Piwik_DataTable_Filter_ReplaceColumnNames extends Piwik_DataTable_Filter $newSubColumns = array(); foreach($columnValue as $idGoal => $goalValues) { + $mapping = Piwik_Archive::$mappingFromIdToNameGoal; + if($idGoal == Piwik_Tracker_GoalManager::IDGOAL_CART) + { + $idGoal = Piwik_Archive::LABEL_ECOMMERCE_CART; + } + elseif($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER) + { + $idGoal = Piwik_Archive::LABEL_ECOMMERCE_ORDER; + } foreach($goalValues as $id => $goalValue) { - $subColumnName = Piwik_Archive::$mappingFromIdToNameGoal[$id]; + $subColumnName = $mapping[$id]; $newSubColumns['idgoal='.$idGoal][$subColumnName] = $goalValue; } } diff --git a/core/Db/Schema/Myisam.php b/core/Db/Schema/Myisam.php index 2f19b54d52075dd878245a3526765b6c1d4518a6..007d174e40270987adb12b2117cadc8e3d2a173a 100644 --- a/core/Db/Schema/Myisam.php +++ b/core/Db/Schema/Myisam.php @@ -176,6 +176,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface visitor_returning TINYINT(1) NOT NULL, visitor_count_visits SMALLINT(5) UNSIGNED NOT NULL, visitor_days_since_last SMALLINT(5) UNSIGNED NOT NULL, + visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL, visitor_days_since_first SMALLINT(5) UNSIGNED NOT NULL, visit_first_action_time DATETIME NOT NULL, visit_last_action_time DATETIME NOT NULL, @@ -186,6 +187,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface visit_total_actions SMALLINT(5) UNSIGNED NOT NULL, visit_total_time SMALLINT(5) UNSIGNED NOT NULL, visit_goal_converted TINYINT(1) NOT NULL, + visit_goal_buyer TINYINT(1) NOT NULL, referer_type TINYINT(1) UNSIGNED NULL, referer_name VARCHAR(70) NULL, referer_url TEXT NOT NULL, @@ -225,6 +227,25 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface INDEX index_idsite_idvisitor (idsite, idvisitor) ) DEFAULT CHARSET=utf8 ", + + 'log_conversion_item' => "CREATE TABLE `{$prefixTables}log_conversion_item` ( + idsite int(10) UNSIGNED NOT NULL, + idvisitor BINARY(8) NOT NULL, + server_time DATETIME NOT NULL, + idvisit INTEGER(10) UNSIGNED NOT NULL, + idorder varchar(100) NOT NULL, + + idaction_sku INTEGER(10) UNSIGNED NOT NULL, + idaction_name INTEGER(10) UNSIGNED NOT NULL, + idaction_category INTEGER(10) UNSIGNED NOT NULL, + price FLOAT NOT NULL, + quantity INTEGER(10) UNSIGNED NOT NULL, + deleted TINYINT(1) UNSIGNED NOT NULL, + + PRIMARY KEY(idvisit, idorder, idaction_sku), + INDEX index_idsite_servertime ( idsite, server_time ) + ) DEFAULT CHARSET=utf8 + ", 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( idvisit int(10) unsigned NOT NULL, @@ -240,13 +261,22 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface visitor_returning tinyint(1) NOT NULL, visitor_count_visits SMALLINT(5) UNSIGNED NOT NULL, visitor_days_since_first SMALLINT(5) UNSIGNED NOT NULL, + visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL, location_country char(3) NOT NULL, location_continent char(3) NOT NULL, url text NOT NULL, idgoal int(10) unsigned NOT NULL, - revenue float default NULL, buster int unsigned NOT NULL, - custom_var_k1 VARCHAR(50) DEFAULT NULL, + + idorder varchar(100) default NULL, + items SMALLINT UNSIGNED DEFAULT NULL, + revenue float default NULL, + revenue_subtotal float default NULL, + revenue_tax float default NULL, + revenue_shipping float default NULL, + revenue_discount float default NULL, + + custom_var_k1 VARCHAR(50) DEFAULT NULL, custom_var_v1 VARCHAR(50) DEFAULT NULL, custom_var_k2 VARCHAR(50) DEFAULT NULL, custom_var_v2 VARCHAR(50) DEFAULT NULL, @@ -257,6 +287,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface custom_var_k5 VARCHAR(50) DEFAULT NULL, custom_var_v5 VARCHAR(50) DEFAULT NULL, PRIMARY KEY (idvisit, idgoal, buster), + UNIQUE KEY unique_idorder (idorder), INDEX index_idsite_datetime ( idsite, server_time ) ) DEFAULT CHARSET=utf8 ", diff --git a/core/Piwik.php b/core/Piwik.php index 7af1de6abd228d833e42f4a242eb67038fcc217b..5bb6ea89e4fda150d108e478c4839a932495ba40 100644 --- a/core/Piwik.php +++ b/core/Piwik.php @@ -52,6 +52,11 @@ class Piwik || Zend_Registry::get('config')->General->enable_processing_unique_visitors_year_and_range ; } + static public function isEcommerceEnabled($idSite) + { + //TODO ECOMMERCE + return true; + } /* * Prefix/unprefix class name */ @@ -1366,7 +1371,7 @@ class Piwik } else { - $precision = 2; + $precision = Piwik_Tracker_GoalManager::REVENUE_PRECISION; $value = sprintf( "%01.".$precision."f", $value); } } diff --git a/core/Tracker/Action.php b/core/Tracker/Action.php index 3efe824275065e77df7d06c8f003703d0259380d..49f635bf772673a58f18bc079cd464f518af3d36 100644 --- a/core/Tracker/Action.php +++ b/core/Tracker/Action.php @@ -22,6 +22,9 @@ interface Piwik_Tracker_Action_Interface { const TYPE_OUTLINK = 2; const TYPE_DOWNLOAD = 3; const TYPE_ACTION_NAME = 4; + const TYPE_ECOMMERCE_ITEM_SKU = 5; + const TYPE_ECOMMERCE_ITEM_NAME = 6; + const TYPE_ECOMMERCE_ITEM_CATEGORY = 7; public function setRequest($requestArray); public function setIdSite( $idSite ); @@ -48,8 +51,8 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface private $idSite; private $timestamp; private $idLinkVisitAction; - private $idActionName = null; - private $idActionUrl = null; + private $idActionName = false; + private $idActionUrl = false; private $actionName; private $actionType; @@ -184,9 +187,12 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface } $parsedUrl['query'] = substr($validQuery,0,-strlen($separator)); $url = Piwik_Common::getParseUrlReverse($parsedUrl); - printDebug('Excluded parameters "'.implode(',',$excludedParameters).'" from URL'); - printDebug(' Before was "'.$originalUrl.'"'); - printDebug(' After is "'.$url.'"'); + printDebug('Excluding parameters "'.implode(',',$excludedParameters).'" from URL'); + if($originalUrl != $url) + { + printDebug(' Before was "'.$originalUrl.'"'); + printDebug(' After is "'.$url.'"'); + } return $url; } @@ -200,12 +206,108 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface static public function getSqlSelectActionId() { - $sql = "SELECT idaction, type + $sql = "SELECT idaction, type, name FROM ".Piwik_Common::prefixTable('log_action') ." WHERE " ." ( hash = CRC32(?) AND name = ? AND type = ? ) "; return $sql; } + + /** + * This function will find the idaction from the lookup table piwik_log_action, + * given an Action name and type. + * + * This is used to record Page URLs, Page Titles, Ecommerce items SKUs, item names, item categories + * + * If the action name does not exist in the lookup table, it will INSERT it + * @param array $actionNamesAndTypes Array of one or many (name,type) + * @return array Returns the input array, with the idaction appended ie. Array of one or many (name,type,idaction) + */ + static public function loadActionId( $actionNamesAndTypes ) + { + // First, we try and select the actions that are already recorded + $sql = self::getSqlSelectActionId(); + $bind = array(); + $i = 0; + foreach($actionNamesAndTypes as &$actionNameType) + { + list($name,$type) = $actionNameType; + if(empty($name)) + { + $actionNameType[] = false; + continue; + } + if($i > 0) + { + $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) "; + } + $bind[] = $name; + $bind[] = $name; + $bind[] = $type; + $i++; + } + // Case URL & Title are empty + if(empty($bind)) + { + return $actionNamesAndTypes; + } + $actionIds = Piwik_Tracker::getDatabase()->fetchAll($sql, $bind); + + // For the Actions found in the lookup table, add the idaction in the array, + // If not found in lookup table, queue for INSERT + $actionsToInsert = array(); + foreach($actionNamesAndTypes as $index => &$actionNameType) + { + list($name,$type) = $actionNameType; + if(empty($name)) { continue; } + $found = false; + foreach($actionIds as $row) + { + if($name == $row['name'] + && $type == $row['type']) + { + $found = true; + $actionNameType[] = $row['idaction']; + continue; + } + } + if(!$found) + { + $actionsToInsert[] = $index; + } + } + + $sql = "INSERT INTO ". Piwik_Common::prefixTable('log_action'). + "( name, hash, type ) VALUES (?,CRC32(?),?)"; + // Then, we insert all new actions in the lookup table + foreach($actionsToInsert as $actionToInsert) + { + list($name,$type) = $actionNamesAndTypes[$actionToInsert]; + + Piwik_Tracker::getDatabase()->query($sql, array($name, $name, $type)); + $actionId = Piwik_Tracker::getDatabase()->lastInsertId(); + printDebug("Recorded a new action (".self::getActionTypeName($type).") in the lookup table: ". $name . " (idaction = ".$actionId.")"); + + $actionNamesAndTypes[$actionToInsert][] = $actionId; + } + return $actionNamesAndTypes; + } + + static public function getActionTypeName($type) + { + switch($type) + { + case self::TYPE_ACTION_URL: return 'Page URL'; break; + case self::TYPE_OUTLINK: return 'Outlink URL'; break; + case self::TYPE_DOWNLOAD: return 'Download URL'; break; + case self::TYPE_ACTION_NAME: return 'Page Title'; break; + case self::TYPE_ECOMMERCE_ITEM_SKU: return 'Ecommerce Item SKU'; break; + case self::TYPE_ECOMMERCE_ITEM_NAME: return 'Ecommerce Item Name'; break; + case self::TYPE_ECOMMERCE_ITEM_CATEGORY: return 'Ecommerce Item Category'; break; + default: throw new Exception("Unexpected action type ".$type); break; + } + } + /** * Loads the idaction of the current action name and the current action url. * These idactions are used in the visitor logging table to link the visit information @@ -214,55 +316,38 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface * * The methods takes care of creating a new record(s) in the action table if the existing * action name and action url doesn't exist yet. - * */ function loadIdActionNameAndUrl() { - if( !is_null($this->idActionUrl) && !is_null($this->idActionName) ) + if( $this->idActionUrl !== false + && $this->idActionName !== false ) { return; } - $idAction = Piwik_Tracker::getDatabase()->fetchAll( - $this->getSqlSelectActionId() - ." OR " - ." ( hash = CRC32(?) AND name = ? AND type = ? ) ", - array($this->getActionName(), $this->getActionName(), $this->getActionNameType(), - $this->getActionUrl(), $this->getActionUrl(), $this->getActionType()) - ); - - if( $idAction !== false ) + $actions = array(); + $action = array($this->getActionName(), $this->getActionNameType()); + if(!is_null($action[1])) { - foreach($idAction as $row) - { - if( $row['type'] == Piwik_Tracker_Action_Interface::TYPE_ACTION_NAME ) - { - $this->idActionName = $row['idaction']; - } - else - { - $this->idActionUrl = $row['idaction']; - } - } + $actions[] = $action; } - - $sql = "INSERT INTO ". Piwik_Common::prefixTable('log_action'). - "( name, hash, type ) VALUES (?,CRC32(?),?)"; - - if( is_null($this->idActionName) - && !is_null($this->getActionNameType()) ) + $action = array($this->getActionUrl(), $this->getActionType()); + if(!is_null($action[1])) { - Piwik_Tracker::getDatabase()->query($sql, - array($this->getActionName(), $this->getActionName(), $this->getActionNameType())); - $this->idActionName = Piwik_Tracker::getDatabase()->lastInsertId(); - printDebug("Recording a new page name in the lookup table: ". $this->idActionName); + $actions[] = $action; } - - if( is_null($this->idActionUrl) ) + $loadedActionIds = self::loadActionId($actions); + + foreach($loadedActionIds as $loadedActionId) { - Piwik_Tracker::getDatabase()->query($sql, - array($this->getActionUrl(), $this->getActionUrl(), $this->getActionType())); - $this->idActionUrl = Piwik_Tracker::getDatabase()->lastInsertId(); - printDebug("Recording a new page URL in the lookup table: ". $this->idActionUrl); + list($name, $type, $actionId) = $loadedActionId; + if($type == $this->getActionType()) + { + $this->idActionUrl = $actionId; + } + elseif($type == $this->getActionNameType()) + { + $this->idActionName = $actionId; + } } } @@ -291,11 +376,10 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface public function record( $idVisit, $visitorIdCookie, $idRefererActionUrl, $idRefererActionName, $timeSpentRefererAction) { $this->loadIdActionNameAndUrl(); - $idActionName = $this->getIdActionName(); - if(is_null($idActionName)) - { - $idActionName = 0; - } + + $idActionName = in_array($this->getActionType(), array(Piwik_Tracker_Action::TYPE_ACTION_NAME, Piwik_Tracker_Action::TYPE_ACTION_URL)) + ? $this->getIdActionName() + : null; Piwik_Tracker::getDatabase()->query( "INSERT INTO ".Piwik_Common::prefixTable('log_link_visit_action') ." (idvisit, idsite, idvisitor, server_time, idaction_url, idaction_name, idaction_url_ref, idaction_name_ref, time_spent_ref_action) @@ -304,8 +388,8 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface $this->idSite, $visitorIdCookie, Piwik_Tracker::getDatetimeFromTimestamp($this->timestamp), - $this->getIdActionUrl(), - $idActionName , + (int)$this->getIdActionUrl(), + $idActionName, $idRefererActionUrl, $idRefererActionName, $timeSpentRefererAction diff --git a/core/Tracker/GoalManager.php b/core/Tracker/GoalManager.php index d32ae8a5b6e54adc34e6d0718ba474e9d3d06339..ec91de3d1594b7220b27803db9fc78e717b2b085 100644 --- a/core/Tracker/GoalManager.php +++ b/core/Tracker/GoalManager.php @@ -16,12 +16,67 @@ */ class Piwik_Tracker_GoalManager { + // log_visit.visit_goal_buyer + const TYPE_BUYER_NONE = 0; + const TYPE_BUYER_ORDERED = 1; + const TYPE_BUYER_OPEN_CART = 2; + const TYPE_BUYER_ORDERED_AND_OPEN_CART = 3; + + // log_conversion.idorder is NULLable, but not log_conversion_item which defaults to zero for carts + const ITEM_IDORDER_ABANDONED_CART = 0; + + // log_conversion.idgoal special values + const IDGOAL_CART = -1; + const IDGOAL_ORDER = 0; + + const REVENUE_PRECISION = 2; + + public $idGoal; + public $requestIsEcommerce; + public $isGoalAnOrder; + /** * @var Piwik_Tracker_Action */ protected $action = null; protected $convertedGoals = array(); + protected $isThereExistingCartInVisit = false; + protected $request; + protected $orderId; + + function init($request) + { + $this->request = $request; + $this->orderId = Piwik_Common::getRequestVar('ec_id', false, 'string', $this->request); + $this->isGoalAnOrder = !empty($this->orderId); + $this->idGoal = Piwik_Common::getRequestVar('idgoal', -1, 'int', $this->request); + $this->requestIsEcommerce = ($this->idGoal == 0); + } + function getBuyerType($existingType = self::TYPE_BUYER_NONE) + { + // Was there a Cart for this visit prior to the order? + $this->isThereExistingCartInVisit = in_array($existingType, + array( Piwik_Tracker_GoalManager::TYPE_BUYER_OPEN_CART, + Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART)); + + if(!$this->requestIsEcommerce) + { + return $existingType; + } + if($this->isGoalAnOrder) + { + return self::TYPE_BUYER_ORDERED; + } + // request is Add to Cart + if($existingType == self::TYPE_BUYER_ORDERED + || $existingType == self::TYPE_BUYER_ORDERED_AND_OPEN_CART) + { + return self::TYPE_BUYER_ORDERED_AND_OPEN_CART; + } + return self::TYPE_BUYER_OPEN_CART; + } + static public function getGoalDefinitions( $idSite ) { $websiteAttributes = Piwik_Common::getCacheWebsiteAttributes( $idSite ); @@ -62,8 +117,11 @@ class Piwik_Tracker_GoalManager } /** + * Look at the URL or Page Title and sees if it matches any existing Goal definition + * * @param int $idSite * @param Piwik_Tracker_Action $action + * @return int Number of goals matched */ function detectGoalsMatchingUrl($idSite, $action) { @@ -149,27 +207,30 @@ class Piwik_Tracker_GoalManager return count($this->convertedGoals) > 0; } - function detectGoalId($idSite, $idGoal, $request) + function detectGoalId($idSite) { if(!$this->isGoalPluginEnabled()) { return false; } $goals = $this->getGoalDefinitions($idSite); - if(!isset($goals[$idGoal])) + if(!isset($goals[$this->idGoal])) { return false; } - $goal = $goals[$idGoal]; + $goal = $goals[$this->idGoal]; - $url = Piwik_Common::getRequestVar( 'url', '', 'string', $request); + $url = Piwik_Common::getRequestVar( 'url', '', 'string', $this->request); $goal['url'] = Piwik_Tracker_Action::excludeQueryParametersFromUrl($url, $idSite); - $goal['revenue'] = Piwik_Common::getRequestVar('revenue', $goal['revenue'], 'float', $request); + $goal['revenue'] = $this->getRevenue(Piwik_Common::getRequestVar('revenue', $goal['revenue'], 'float', $this->request)); $this->convertedGoals[] = $goal; return true; } - function recordGoals($idSite, $visitorInformation, $visitCustomVariables, $action, $referrerTimestamp, $referrerUrl, $referrerCampaignName, $referrerCampaignKeyword) + /** + * Records one or several goals matched in this request. + */ + public function recordGoals($idSite, $visitorInformation, $visitCustomVariables, $action, $referrerTimestamp, $referrerUrl, $referrerCampaignName, $referrerCampaignKeyword) { $location_country = isset($visitorInformation['location_country']) ? $visitorInformation['location_country'] @@ -191,6 +252,7 @@ class Piwik_Tracker_GoalManager 'location_continent'=> $location_continent, 'visitor_returning' => $visitorInformation['visitor_returning'], 'visitor_days_since_first' => $visitorInformation['visitor_days_since_first'], + 'visitor_days_since_order' => $visitorInformation['visitor_days_since_order'], 'visitor_count_visits' => $visitorInformation['visitor_count_visits'], ); @@ -240,17 +302,375 @@ class Piwik_Tracker_GoalManager $goal += $visitCustomVariables; + // some goals are converted, so must be ecommerce Order or Cart Update + if($this->requestIsEcommerce) + { + $this->recordEcommerceGoal($goal, $visitorInformation); + } + else + { + $this->recordStandardGoals($goal, $action, $visitorInformation); + } + } + + /** + * Returns rounded decimal revenue, or if revenue is integer, then returns as is. + * + * @param int|float $revenue + * @return int|float + */ + protected function getRevenue($revenue) + { + if(round($revenue) == $revenue) + { + return $revenue; + } + return round($revenue, self::REVENUE_PRECISION); + } + + /** + * Records an Ecommerce conversion in the DB. Deals with Items found in the request. + * Will deal with 2 types of conversions: Ecommerce Order and Ecommerce Cart update (Add to cart, Update Cart etc). + * + * @param array $goal + * @param array $visitorInformation + */ + protected function recordEcommerceGoal($goal, $visitorInformation) + { + // Is the transaction a Cart Update or an Ecommerce order? + $updateWhere = array( + 'idvisit' => $visitorInformation['idvisit'], + 'idgoal' => self::IDGOAL_CART, + 'buster' => 0, + ); + + if($this->isThereExistingCartInVisit) + { + printDebug("There is an existing cart for this visit"); + } + if($this->isGoalAnOrder) + { + // If Order, make sure that we don't record the same order twice by using the order ID as a buster + $orderIdHash = substr(md5($this->orderId), 0, 8); + $orderIdNumeric = base_convert($orderIdHash, 16, 10); + $goal['idgoal'] = self::IDGOAL_ORDER; + $goal['idorder'] = $this->orderId; + $goal['buster'] = $orderIdNumeric; + $goal['revenue_subtotal'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_st', false, 'float', $this->request)); + $goal['revenue_tax'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_tx', false, 'float', $this->request)); + $goal['revenue_shipping'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_sh', false, 'float', $this->request)); + $goal['revenue_discount'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_dt', false, 'float', $this->request)); + + $debugMessage = 'The conversion is an Ecommerce order'; + } + // If Cart update, select current items in the previous Cart + else + { + $goal['buster'] = 0; + $goal['idgoal'] = self::IDGOAL_CART; + $debugMessage = 'The conversion is an Ecommerce Cart Update'; + } + $goal['revenue'] = $this->getRevenue(Piwik_Common::getRequestVar('revenue', 0, 'float', $this->request)); + + printDebug($debugMessage . ':' . var_export($goal, true)); + + // INSERT or Sync items in the Cart / Order for this visit & order + $items = $this->getEcommerceItemsFromRequest(); + if($items === false) + { + return; + } + + $itemsCount = 0; + foreach($items as $item) + { + $itemsCount += $item[self::INDEX_ITEM_QUANTITY]; + } + $goal['items'] = $itemsCount; + + // If there is already a cart for this visit + // 1) If conversion is Order, we update the cart into an Order + // 2) If conversion is Cart Update, we update the cart + $recorded = $this->recordGoal($goal, $this->isThereExistingCartInVisit, $updateWhere); + if($recorded) + { + $this->recordEcommerceItems($goal, $items); + } + } + + /** + * Returns Items read from the request string + * @return array|false + */ + protected function getEcommerceItemsFromRequest() + { + $items = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('ec_items', '', 'string', $this->request)); + if(empty($items)) + { + printDebug("There are no Ecommerce items in the request"); + // we still record an Ecommerce order without any item in it + return array(); + } + $items = json_decode($items, $assoc = true); + if(!is_array($items)) + { + printDebug("Error while json_decode the Ecommerce items = ".var_export($items, true)); + return false; + } + + $cleanedItems = $this->getCleanedEcommerceItems($items); + return $cleanedItems; + } + + /** + * Loads the Ecommerce items from the request and records them in the DB + * + * @param array $goal + * @return int $items Number of items in the cart + */ + protected function recordEcommerceItems($goal, $items) + { + $itemBySku = array(); + foreach($items as $item) + { + $itemBySku[$item[0]] = $item; + } +// var_dump($items); echo "Items by SKU:";var_dump($itemBySku); + + // Select all items currently in the Cart if any + $sql = "SELECT idaction_sku, idaction_name, idaction_category, price, quantity + FROM ". Piwik_Common::prefixTable('log_conversion_item') . " + WHERE idvisit = ? + AND idorder = ? + AND deleted = 0"; + $bind = array($goal['idvisit'], isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART); + $itemsInDb = Piwik_Tracker::getDatabase()->fetchAll($sql, $bind); + + // Look at which items need to be deleted, which need to be added or updated, based on the SKU + $skuFoundInDb = $itemsToUpdate = array(); + foreach($itemsInDb as $itemInDb) + { + $skuFoundInDb[] = $itemInDb['idaction_sku']; + + // Ensure price comparisons will have the same assumption + $itemInDb['price'] = $this->getRevenue($itemInDb['price']); + $itemInDb = array_values($itemInDb); + + // Cast all as string, because what comes out of the fetchAll() are strings + $itemInDb = $this->getItemRowCast($itemInDb); + + //Item in the cart in the DB, but not anymore in the cart + if(!isset($itemBySku[$itemInDb[0]])) + { + $itemsToUpdate[] = $itemInDb + array('deleted' => 1); + continue; + } + + $newItem = $itemBySku[$itemInDb[0]]; + $newItem = $this->getItemRowCast($newItem); + + if(count($itemInDb) != count($newItem)) + { + throw new Exception(" Item in DB and Item in cart have a different format, this is not expected... ".var_export($itemInDb, true) . var_export($newItem, true)); + } +// echo "NEW ITEM:";var_dump($newItem); echo "ITEM IN DB:";var_dump($itemInDb); + if($newItem != $itemInDb) + { +// echo "The following item is out of date in the DB: DB VERSION:"; var_dump($itemInDb); echo "NEW ITEM:"; var_dump($newItem); + $itemsToUpdate[] = $newItem; + } + } + + // Items to UPDATE + $this->updateEcommerceItems($goal, $itemsToUpdate); + + // Items to INSERT + $itemsToInsert = array(); + foreach($items as $item) + { + if(!in_array($item[0], $skuFoundInDb)) + { + $itemsToInsert[] = $item; + } + } + $this->insertEcommerceItems($goal, $itemsToInsert); + } + + const INDEX_ITEM_SKU = 0; + const INDEX_ITEM_NAME = 1; + const INDEX_ITEM_CATEGORY = 2; + const INDEX_ITEM_PRICE = 3; + const INDEX_ITEM_QUANTITY = 4; + + /** + * Reads items from the request, then looks up the names from the lookup table + * and returns a clean array of items ready for the database. + * + * @param array $items + * @return array $cleanedItems + */ + protected function getCleanedEcommerceItems($items) + { + // Clean up the items array + $cleanedItems = array(); + foreach($items as $item) + { + $name = $category = false; + $price = 0; + $quantity = 1; + // items are passed in the request as an array: ( $sku, $name, $category, $price, $quantity ) + if(empty($item[self::INDEX_ITEM_SKU])) { + continue; + } + + $sku = $item[self::INDEX_ITEM_SKU]; + if(!empty($item[self::INDEX_ITEM_NAME])) { + $name = $item[self::INDEX_ITEM_NAME]; + } + if(!empty($item[self::INDEX_ITEM_CATEGORY])) { + $category = $item[self::INDEX_ITEM_CATEGORY]; + } + if(!empty($item[self::INDEX_ITEM_PRICE]) + && is_numeric($item[self::INDEX_ITEM_PRICE])) { + $price = $this->getRevenue($item[self::INDEX_ITEM_PRICE]); + } + if(!empty($item[self::INDEX_ITEM_QUANTITY]) + && is_numeric($item[self::INDEX_ITEM_QUANTITY])) { + $quantity = (int)$item[self::INDEX_ITEM_QUANTITY]; + } + + // self::INDEX_ITEM_* are in order + $cleanedItems[] = array( $sku, $name, $category, $price, $quantity ); + } + + // Lookup Item SKUs, Names & Categories Ids + $actionsToLookup = array(); + foreach($cleanedItems as $item) + { + list($sku, $name, $category, $price, $quantity) = $item; + $actionsToLookup[] = array($sku, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_SKU); + $actionsToLookup[] = array($name, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_NAME); + $actionsToLookup[] = array($category, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY); + } + + $actionsLookedUp = Piwik_Tracker_Action::loadActionId($actionsToLookup); +// var_dump($actionsLookedUp); + + // Replace SKU, name & category by their ID action + foreach($cleanedItems as $index => &$item) + { + list($sku, $name, $category, $price, $quantity) = $item; + + // SKU + $item[0] = $actionsLookedUp[ $index * 3 + self::INDEX_ITEM_SKU][2]; + // Name + $item[1] = $actionsLookedUp[ $index * 3 + self::INDEX_ITEM_NAME][2]; + // Category + $item[2] = $actionsLookedUp[ $index * 3 + self::INDEX_ITEM_CATEGORY][2]; + } + return $cleanedItems; + } + + /** + * Updates the cart items in the DB + * that have been modified since the last cart update + */ + protected function updateEcommerceItems($goal, $itemsToUpdate) + { + if(empty($itemsToUpdate)) + { + return; + } + printDebug("Ecommerce items that are updated in the cart:"); + printDebug($itemsToUpdate); + + foreach($itemsToUpdate as $item) + { + $newRow = $this->getItemRowEnriched($goal, $item); + $updateParts = $sqlBind = array(); + foreach($newRow AS $name => $value) + { + $updateParts[] = $name." = ?"; + $sqlBind[] = $value; + } + $sql = 'UPDATE ' . Piwik_Common::prefixTable('log_conversion_item') . " + SET ".implode($updateParts, ', ')." + WHERE idvisit = ? + AND idorder = ? + AND idaction_sku = ?"; + $sqlBind[] = $newRow['idvisit']; + $sqlBind[] = $newRow['idorder']; + $sqlBind[] = $newRow['idaction_sku']; + Piwik_Tracker::getDatabase()->query($sql, $sqlBind); + } + } + + /** + * Inserts in the cart in the DB the new items + * that were not previously in the cart + */ + protected function insertEcommerceItems($goal, $itemsToInsert) + { + if(empty($itemsToInsert)) + { + return; + } + printDebug("Ecommerce items that are added to the cart/order"); + printDebug($itemsToInsert); + + $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_conversion_item') . " + (idaction_sku, idaction_name, idaction_category, price, quantity, deleted, + idorder, idsite, idvisitor, server_time, idvisit) + VALUES "; + $i = 0; + $bind = array(); + foreach($itemsToInsert as $item) + { + if($i > 0) { $sql .= ','; } + + $newRow = array_values($this->getItemRowEnriched($goal, $item)); + $sql .= " ( ". Piwik_Common::getSqlStringFieldsArray($newRow) . " ) "; + $i++; + $bind = array_merge($bind, $newRow); + } + Piwik_Tracker::getDatabase()->query($sql, $bind); + printDebug($sql);printDebug($bind); + } + + protected function getItemRowEnriched($goal, $item) + { + $newRow = array( + 'idaction_sku' => $item[0], + 'idaction_name' => $item[1], + 'idaction_category' => $item[2], + 'price' => $item[3], + 'quantity' => $item[4], + 'deleted' => isset($item['deleted']) ? $item['deleted'] : 0, //deleted + 'idorder' => isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, //idorder = 0 in log_conversion_item for carts + 'idsite' => $goal['idsite'], + 'idvisitor' => $goal['idvisitor'], + 'server_time' => $goal['server_time'], + 'idvisit' => $goal['idvisit'] + ); + return $newRow; + } + /** + * Records a standard non-Ecommerce goal in the DB (URL/Title matching), + * linking the conversion to the action that triggered it + */ + protected function recordStandardGoals($goal, $action, $visitorInformation) + { foreach($this->convertedGoals as $convertedGoal) { printDebug("- Goal ".$convertedGoal['idgoal'] ." matched. Recording..."); $newGoal = $goal; $newGoal['idgoal'] = $convertedGoal['idgoal']; $newGoal['url'] = $convertedGoal['url']; - $newGoal['revenue'] = $convertedGoal['revenue']; + $newGoal['revenue'] = $this->getRevenue($convertedGoal['revenue']); if(!is_null($action)) { - $newGoal['idaction_url'] = $action->getIdActionUrl(); + $newGoal['idaction_url'] = (int)$action->getIdActionUrl(); $newGoal['idlink_va'] = $action->getIdLinkVisitAction(); } @@ -258,17 +678,68 @@ class Piwik_Tracker_GoalManager $newGoal['buster'] = $convertedGoal['allow_multiple'] == 0 ? '0' : $visitorInformation['visit_last_action_time']; - $newGoalDebug = $newGoal; - $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']); - printDebug($newGoalDebug); + + $this->recordGoal($newGoal); + } + } + /** + * Helper function used by other record* methods which will INSERT or UPDATE the conversion in the DB + * + * @param array $newGoal + * @param bool $mustUpdateNotInsert If set to true, the previous conversion will be UPDATEd. This is used for the Cart Update conversion (only one cart per visit) + * @param array $updateWhere + */ + protected function recordGoal($newGoal, $mustUpdateNotInsert = false, $updateWhere = array()) + { + $newGoalDebug = $newGoal; + $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']); + printDebug($newGoalDebug); - $fields = implode(", ", array_keys($newGoal)); - $bindFields = substr(str_repeat( "?,",count($newGoal)),0,-1); - - $sql = "INSERT IGNORE INTO " . Piwik_Common::prefixTable('log_conversion') . " + $fields = implode(", ", array_keys($newGoal)); + $bindFields = Piwik_Common::getSqlStringFieldsArray($newGoal); + + if($mustUpdateNotInsert) + { + $updateParts = $sqlBind = $updateWhereParts = array(); + foreach($newGoal AS $name => $value) + { + $updateParts[] = $name." = ?"; + $sqlBind[] = $value; + } + foreach($updateWhere as $name => $value) + { + $updateWhereParts[] = $name." = ?"; + $sqlBind[] = $value; + } + $sql = 'UPDATE ' . Piwik_Common::prefixTable('log_conversion') . " + SET ".implode($updateParts, ', ')." + WHERE ".implode($updateWhereParts, ' AND '); + Piwik_Tracker::getDatabase()->query($sql, $sqlBind); + return true; + } + else + { + $sql = 'INSERT IGNORE INTO ' . Piwik_Common::prefixTable('log_conversion') . " ($fields) VALUES ($bindFields) "; $bind = array_values($newGoal); - Piwik_Tracker::getDatabase()->query($sql, $bind); + $result = Piwik_Tracker::getDatabase()->query($sql, $bind); + + // If a record was inserted, we return true + return Piwik_Tracker::getDatabase()->rowCount($result) > 0; } } + + /** + * Casts the item array so that array comparisons work nicely + */ + protected function getItemRowCast($row) + { + return array( + (string)(int)$row[0], + (string)(int)$row[1], + (string)(int)$row[2], + (string)$row[3], + (string)$row[4], + ); + } } diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php index 089477439d7811a3d408711e7ea596f557f16286..1162ea15853c452c787022abe6b687959829764e 100644 --- a/core/Tracker/Visit.php +++ b/core/Tracker/Visit.php @@ -50,6 +50,11 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface protected $ip; // via setForcedVisitorId() protected $forcedVisitorId; + + /** + * @var Piwik_Tracker_GoalManager + */ + protected $goalManager; const TIME_IN_PAST_TO_SEARCH_FOR_VISITOR = 86400; @@ -132,22 +137,36 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface return; } $this->visitorCustomVariables = $this->getCustomVariables(); - $goalManager = new Piwik_Tracker_GoalManager(); - $someGoalsConverted = false; - $idActionUrl = $idActionName = 0; + $this->goalManager = new Piwik_Tracker_GoalManager(); + + $someGoalsConverted = $visitIsConverted = false; + $idActionUrl = $idActionName = false; $action = null; - $idGoal = Piwik_Common::getRequestVar('idgoal', 0, 'int', $this->request); - $requestIsManualGoalConversion = ($idGoal > 0); + $this->goalManager->init($this->request); + + $requestIsManualGoalConversion = ($this->goalManager->idGoal > 0); + + if($this->goalManager->requestIsEcommerce) + { + $someGoalsConverted = true; + + // Mark the visit as Converted only if it is an order (not for a Cart update) + if($this->goalManager->isGoalAnOrder) + { + $visitIsConverted = true; + } + } // this request is from the JS call to piwikTracker.trackGoal() - if($requestIsManualGoalConversion) + elseif($requestIsManualGoalConversion) { - $someGoalsConverted = $goalManager->detectGoalId($this->idsite, $idGoal, $this->request); + $someGoalsConverted = $this->goalManager->detectGoalId($this->idsite); + $visitIsConverted = $someGoalsConverted; // if we find a idgoal in the URL, but then the goal is not valid, this is most likely a fake request if(!$someGoalsConverted) { - printDebug('Invalid goal tracking request for goal id = '.$idGoal); - unset($goalManager); + printDebug('Invalid goal tracking request for goal id = '.$this->goalManager->idGoal); + unset($this->goalManager); return; } } @@ -156,11 +175,12 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface { $action = $this->newAction(); $this->handleAction($action); - $someGoalsConverted = $goalManager->detectGoalsMatchingUrl($this->idsite, $action); - + $someGoalsConverted = $this->goalManager->detectGoalsMatchingUrl($this->idsite, $action); + $visitIsConverted = $someGoalsConverted; + $action->loadIdActionNameAndUrl(); - $idActionUrl = $action->getIdActionUrl(); - $idActionName = $action->getIdActionName(); + $idActionUrl = (int)$action->getIdActionUrl(); + $idActionName = (int)$action->getIdActionName(); } // the visitor and session @@ -181,7 +201,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $idRefererActionUrl = $this->visitorInfo['visit_exit_idaction_url']; $idRefererActionName = $this->visitorInfo['visit_exit_idaction_name']; try { - $this->handleKnownVisit($idActionUrl, $idActionName, $someGoalsConverted); + $this->handleKnownVisit($idActionUrl, $idActionName, $visitIsConverted); if(!is_null($action)) { $action->record( $this->visitorInfo['idvisit'], @@ -201,7 +221,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface // In this case, we cancel the current conversion to be recorded: if($requestIsManualGoalConversion) { - $someGoalsConverted = false; + $someGoalsConverted = $visitIsConverted = false; } // When the row wasn't found in the logs, and this is a pageview or // goal matching URL, we force a new visitor @@ -219,7 +239,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface if(!$this->isVisitorKnown() || !$isLastActionInTheSameVisit) { - $this->handleNewVisit($idActionUrl, $idActionName, $someGoalsConverted); + $this->handleNewVisit($idActionUrl, $idActionName, $visitIsConverted); if(!is_null($action)) { $action->record( $this->visitorInfo['idvisit'], $this->visitorInfo['idvisitor'], 0, 0, 0 ); @@ -237,7 +257,8 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $refererCampaignName = Piwik_Common::getRequestVar('_rcn', '', 'string', $this->request); $refererCampaignKeyword = Piwik_Common::getRequestVar('_rck', '', 'string', $this->request); - $goalManager->recordGoals( $this->idsite, + $this->goalManager->recordGoals( + $this->idsite, $this->visitorInfo, $this->visitorCustomVariables, $action, @@ -247,7 +268,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $refererCampaignKeyword ); } - unset($goalManager); + unset($this->goalManager); unset($action); $this->printCookie(); } @@ -270,20 +291,10 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface } if(isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) { - switch($action->getActionType()) { - case Piwik_Tracker_Action::TYPE_ACTION_URL: - $type = "normal page view"; - break; - case Piwik_Tracker_Action::TYPE_DOWNLOAD: - $type = "download"; - break; - case Piwik_Tracker_Action::TYPE_OUTLINK: - $type = "outlink"; - break; - } - printDebug("Action is a <u>$type</u>,". - "\n Action name: ". $action->getActionName() . ",". - "\n Action URL = ". $action->getActionUrl() ); + $type = Piwik_Tracker_Action::getActionTypeName($action->getActionType()); + printDebug("Action is a $type, + Action name = ". $action->getActionName() . ", + Action URL = ". $action->getActionUrl() ); } } @@ -301,25 +312,21 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface * Tracker.knownVisitorInformation is triggered after saving the new visit data * Even data is an array with updated information about the visit */ - protected function handleKnownVisit($idActionUrl, $idActionName, $someGoalsConverted) + protected function handleKnownVisit($idActionUrl, $idActionName, $visitIsConverted) { // gather information that needs to be updated $valuesToUpdate = array(); - if($someGoalsConverted) + if($visitIsConverted) { $valuesToUpdate['visit_goal_converted'] = 1; } $sqlActionUpdate = ''; - if(!empty($idActionUrl)) + if($idActionUrl !== false) { $valuesToUpdate['visit_exit_idaction_url'] = $idActionUrl; $sqlActionUpdate = "visit_total_actions = visit_total_actions + 1, "; - if(empty($idActionName)) - { - $idActionName = 0; - } - $valuesToUpdate['visit_exit_idaction_name'] = $idActionName; + $valuesToUpdate['visit_exit_idaction_name'] = (int)$idActionName; } $datetimeServer = Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()); @@ -344,6 +351,9 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $valuesToUpdate['idvisitor'] = Piwik_Common::hex2bin($idVisitor); } + // Ecommerce buyer status + $valuesToUpdate['visit_goal_buyer'] = $this->goalManager->getBuyerType($this->visitorInfo['visit_goal_buyer']); + // Custom Variables overwrite previous values on each page view $valuesToUpdate = array_merge($valuesToUpdate, $this->visitorCustomVariables); @@ -372,7 +382,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $updateParts[] = $name." = ?"; $sqlBind[] = $value; } - +//var_dump($valuesToUpdate);exit; $sqlQuery = "UPDATE ". Piwik_Common::prefixTable('log_visit')." SET $sqlActionUpdate ".implode($updateParts, ', ')." WHERE idsite = ? @@ -413,7 +423,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface * * 2) Insert the visit information */ - protected function handleNewVisit($idActionUrl, $idActionName, $someGoalsConverted) + protected function handleNewVisit($idActionUrl, $idActionName, $visitIsConverted) { printDebug("New Visit (IP = ".Piwik_IP::N2P($this->getVisitorIp()).")"); @@ -456,6 +466,14 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $daysSinceLastVisit = round(($this->getCurrentTimestamp() - $lastVisitTimestamp)/86400, $precision = 0); if($daysSinceLastVisit < 0) $daysSinceLastVisit = 0; } + + $daysSinceLastOrder = 0; + $lastOrderTimestamp = Piwik_Common::getRequestVar('_ects', 0, 'int', $this->request); + if($this->isTimestampValid($lastOrderTimestamp)) + { + $daysSinceLastOrder = round(($this->getCurrentTimestamp() - $lastOrderTimestamp)/86400, $precision = 0); + if($daysSinceLastOrder < 0) $daysSinceLastOrder = 0; + } // User settings $userInfo = $this->getUserSettingsInformation(); @@ -479,6 +497,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface 'visitor_returning' => $visitCount > 1 || $this->isVisitorKnown() ? 1 : 0, 'visitor_count_visits' => $visitCount, 'visitor_days_since_last' => $daysSinceLastVisit, + 'visitor_days_since_order' => $daysSinceLastOrder, 'visitor_days_since_first' => $daysSinceFirstVisit, 'visit_first_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()), 'visit_last_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()), @@ -488,7 +507,8 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface 'visit_exit_idaction_name' => (int)$idActionName, 'visit_total_actions' => 1, 'visit_total_time' => $defaultTimeOnePageVisit, - 'visit_goal_converted' => $someGoalsConverted ? 1: 0, + 'visit_goal_converted' => $visitIsConverted ? 1: 0, + 'visit_goal_buyer' => $this->goalManager->getBuyerType(), 'referer_type' => $refererInfo['referer_type'], 'referer_name' => $refererInfo['referer_name'], 'referer_url' => Piwik_Common::unsanitizeInputValue($refererInfo['referer_url']), @@ -545,7 +565,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $this->visitorInfo['config_resolution'] = substr($this->visitorInfo['config_resolution'], 0, 9); $fields = implode(", ", array_keys($this->visitorInfo)); - $values = substr(str_repeat( "?,",count($this->visitorInfo)),0,-1); + $values = Piwik_Common::getSqlStringFieldsArray($this->visitorInfo); $sql = "INSERT INTO ".Piwik_Common::prefixTable('log_visit'). " ($fields) VALUES ($values)"; $bind = array_values($this->visitorInfo); @@ -895,10 +915,12 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface visit_exit_idaction_name, visitor_returning, visitor_days_since_first, + visitor_days_since_order, referer_name, referer_keyword, referer_type, - visitor_count_visits + visitor_count_visits, + visit_goal_buyer FROM ".Piwik_Common::prefixTable('log_visit'). " WHERE ".$where." ORDER BY visit_last_action_time DESC @@ -918,8 +940,10 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $this->visitorInfo['visit_exit_idaction_name'] = $visitRow['visit_exit_idaction_name']; $this->visitorInfo['visitor_returning'] = $visitRow['visitor_returning']; $this->visitorInfo['visitor_days_since_first'] = $visitRow['visitor_days_since_first']; + $this->visitorInfo['visitor_days_since_order'] = $visitRow['visitor_days_since_order']; $this->visitorInfo['visitor_count_visits'] = $visitRow['visitor_count_visits']; - + $this->visitorInfo['visit_goal_buyer'] = $visitRow['visit_goal_buyer']; + // Referer information will be potentially used for Goal Conversion attribution $this->visitorInfo['referer_name'] = $visitRow['referer_name']; $this->visitorInfo['referer_keyword'] = $visitRow['referer_keyword']; @@ -930,7 +954,8 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface config_id = ".bin2hex($configId).", idvisit = {$this->visitorInfo['idvisit']}, last action = ".date("r", $this->visitorInfo['visit_last_action_time']).", - first action = ".date("r", $this->visitorInfo['visit_first_action_time']) .")"); + first action = ".date("r", $this->visitorInfo['visit_first_action_time']) .", + visit_goal_buyer' = ".$this->visitorInfo['visit_goal_buyer'].")"); } else { diff --git a/core/Updates/1.5-b1.php b/core/Updates/1.5-b1.php new file mode 100644 index 0000000000000000000000000000000000000000..3fcd4a6937a7e74c286a1feb7b8153bf17295d70 --- /dev/null +++ b/core/Updates/1.5-b1.php @@ -0,0 +1,79 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * @version $Id$ + * + * @category Piwik + * @package Updates + */ + +/** + * @package Updates + */ +class Piwik_Updates_1_5_b1 extends Piwik_Updates +{ + static function getSql($schema = 'Myisam') + { + $tables = Piwik::getTablesCreateSql(); + + return array( + 'CREATE TABLE `'. Piwik_Common::prefixTable('log_conversion_item') .'` ( + idsite int(10) UNSIGNED NOT NULL, + idvisitor BINARY(8) NOT NULL, + server_time DATETIME NOT NULL, + idvisit INTEGER(10) UNSIGNED NOT NULL, + idorder varchar(100) NOT NULL, + + idaction_sku INTEGER(10) UNSIGNED NOT NULL, + idaction_name INTEGER(10) UNSIGNED NOT NULL, + idaction_category INTEGER(10) UNSIGNED NOT NULL, + price FLOAT NOT NULL, + quantity INTEGER(10) UNSIGNED NOT NULL, + deleted TINYINT(1) UNSIGNED NOT NULL, + + PRIMARY KEY(idvisit, idorder, idaction_sku), + INDEX index_idsite_servertime ( idsite, server_time ) + ) DEFAULT CHARSET=utf8' => false, + 'ALTER IGNORE TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_last, + ADD visit_goal_buyer TINYINT(1) NOT NULL AFTER visit_goal_converted' => false, + 'ALTER IGNORE TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_first, + ADD idorder varchar(100) default NULL AFTER buster, + ADD items SMALLINT UNSIGNED DEFAULT NULL, + ADD revenue_subtotal float default NULL, + ADD revenue_tax float default NULL, + ADD revenue_shipping float default NULL, + ADD revenue_discount float default NULL, + ADD UNIQUE KEY unique_idorder (idorder)' => false, + + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + + $obsoleteFile = '/plugins/ExamplePlugin/API.php'; + if(file_exists(PIWIK_INCLUDE_PATH . $obsoleteFile)) + { + @unlink(PIWIK_INCLUDE_PATH . $obsoleteFile); + } + + $obsoleteDirectories = array( + '/plugins/AdminHome', + '/plugins/Home', + '/plugins/PluginsAdmin', + ); + foreach($obsoleteDirectories as $dir) + { + if(file_exists(PIWIK_INCLUDE_PATH . $dir)) + { + Piwik::unlinkRecursive(PIWIK_INCLUDE_PATH . $dir, true); + } + } + } +} diff --git a/core/Version.php b/core/Version.php index d0307259976a94d2b5ec7d0c7486eb5fe130140f..f9cabbd42d00cdeae71ab8507dffb576bbc881b9 100644 --- a/core/Version.php +++ b/core/Version.php @@ -17,5 +17,5 @@ */ final class Piwik_Version { - const VERSION = '1.4'; + const VERSION = '1.5-b1'; } diff --git a/js/piwik.js b/js/piwik.js index 510e8f312e9a7f2b4fac241cf39f0b3f97702351..0e1c0e1beaa61e05b1d802d48c54c05c78329b02 100644 --- a/js/piwik.js +++ b/js/piwik.js @@ -402,7 +402,7 @@ if (!this.JSON2) { doNotTrack, setDoNotTrack, addListener, enableLinkTracking, setLinkTrackingTimer, setHeartBeatTimer, killFrame, redirectFile, - trackGoal, trackLink, trackPageView, + trackGoal, trackLink, trackPageView, addEcommerceItem, trackEcommerceOrder, trackEcommerceCartUpdate, addPlugin, getTracker, getAsyncTracker */ var @@ -1010,6 +1010,9 @@ var // Maximum number of custom variables maxCustomVariables = 5, + // Ecommerce items + ecommerceItems = {}, + // Browser features via client-side data collection browserFeatures = {}, @@ -1239,8 +1242,8 @@ var * Sets the Visitor ID cookie: either the first time loadVisitorIdCookie is called * or when there is a new visit or a new page view */ - function setVisitorIdCookie(uuid, createTs, visitCount, nowTs, lastVisitTs) { - setCookie(getCookieName('id'), uuid + '.' + createTs + '.' + visitCount + '.' + nowTs + '.' + lastVisitTs, configVisitorCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); + function setVisitorIdCookie(uuid, createTs, visitCount, nowTs, lastVisitTs, lastEcommerceOrderTs) { + setCookie(getCookieName('id'), uuid + '.' + createTs + '.' + visitCount + '.' + nowTs + '.' + lastVisitTs + '.' + lastEcommerceOrderTs, configVisitorCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); } /* @@ -1285,6 +1288,9 @@ var nowTs, // last visit timestamp - blank = no previous visit + '', + + // last ecommerce order timestamp '' ]; } @@ -1329,7 +1335,7 @@ var * with the standard parameters (plugins, resolution, url, referrer, etc.). * Sends the pageview and browser settings with every request in case of race conditions. */ - function getRequest(request, customData, pluginMethod) { + function getRequest(request, customData, pluginMethod, currentEcommerceOrderTs) { var i, now = new Date(), nowTs = Math.round(now.getTime() / 1000), @@ -1339,6 +1345,7 @@ var createTs, currentVisitTs, lastVisitTs, + lastEcommerceOrderTs, referralTs, referralUrl, referralUrlMaxLength = 1024, @@ -1370,6 +1377,15 @@ var visitCount = id[3]; currentVisitTs = id[4]; lastVisitTs = id[5]; + // case migrating from pre-1.5 cookies + if(!isDefined(id[6])) { + id[6] = ""; + } + lastEcommerceOrderTs = id[6]; + + if(!isDefined(currentEcommerceOrderTs)) { + currentEcommerceOrderTs = ""; + } campaignNameDetected = attributionCookie[0]; campaignKeywordDetected = attributionCookie[1]; @@ -1432,21 +1448,21 @@ var setCookie(refname, JSON2.stringify(attributionCookie), configReferralCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); } } - // build out the rest of the request request += '&idsite=' + configTrackerSiteId + '&rec=1' + - '&rand=' + Math.random() + + '&r=' + String(Math.random()).slice(2,8) + // keep the string to a minimum '&h=' + now.getHours() + '&m=' + now.getMinutes() + '&s=' + now.getSeconds() + '&url=' + encodeWrapper(purify(currentUrl)) + - '&urlref=' + encodeWrapper(purify(configReferrerUrl)) + + (configReferrerUrl.length ? '&urlref=' + encodeWrapper(purify(configReferrerUrl)) : '') + '&_id=' + uuid + '&_idts=' + createTs + '&_idvc=' + visitCount + '&_idn=' + newVisitor + // currently unused - '&_rcn=' + encodeWrapper(campaignNameDetected) + - '&_rck=' + encodeWrapper(campaignKeywordDetected) + + (campaignNameDetected.length ? '&_rcn=' + encodeWrapper(campaignNameDetected) : '') + + (campaignKeywordDetected.length ? '&_rck=' + encodeWrapper(campaignKeywordDetected) : '') + '&_refts=' + referralTs + '&_viewts=' + lastVisitTs + - '&_ref=' + encodeWrapper(purify(referralUrl.slice(0, referralUrlMaxLength))) + (String(lastEcommerceOrderTs).length ? '&_ects=' + lastEcommerceOrderTs : '') + + (String(referralUrl).length ? '&_ref=' + encodeWrapper(purify(referralUrl.slice(0, referralUrlMaxLength))) : '') ; // browser features @@ -1463,9 +1479,12 @@ var request += '&data=' + encodeWrapper(JSON2.stringify(configCustomData)); } - // Don't send custom variables if empty if (customVariables) { - request += '&_cvar=' + encodeWrapper(JSON2.stringify(customVariables)); + var customVariablesStringified = JSON2.stringify(customVariables); + // Don't sent empty custom variables {} + if(customVariablesStringified.length > 2) { + request += '&_cvar=' + encodeWrapper(customVariablesStringified); + } // Don't save deleted custom variables in the cookie for (i in customVariablesCopy) { @@ -1480,7 +1499,7 @@ var } // update cookies - setVisitorIdCookie(uuid, createTs, visitCount, nowTs, lastVisitTs); + setVisitorIdCookie(uuid, createTs, visitCount, nowTs, lastVisitTs, isDefined(currentEcommerceOrderTs) && String(currentEcommerceOrderTs).length ? currentEcommerceOrderTs : lastEcommerceOrderTs); setCookie(sesname, '*', configSessionCookieTimeout, configCookiePath, configCookieDomain, cookieSecure); // tracker plugin hook @@ -1488,7 +1507,76 @@ var return request; } + + function logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount) { + var request = 'idgoal=0', + lastEcommerceOrderTs, + now = new Date(), + items = [], + sku; + + if(String(orderId).length) { + request += '&ec_id=' + encodeWrapper(orderId); + // Record date of order in the visitor cookie + lastEcommerceOrderTs = Math.round(now.getTime() / 1000); + } + + request += '&revenue=' + grandTotal; + if(String(subTotal).length) { + request += '&ec_st=' + subTotal; + } + if(String(tax).length) { + request += '&ec_tx=' + tax; + } + if(String(shipping).length) { + request += '&ec_sh=' + shipping; + } + if(String(discount).length) { + request += '&ec_dt=' + discount; + } + if(ecommerceItems) { + // Removing the SKU index in the array before JSON encoding + for(sku in ecommerceItems) { + if (Object.prototype.hasOwnProperty.call(ecommerceItems, sku)) { + // Ensure name and category default to healthy value + if(!isDefined(ecommerceItems[sku][1])) { + ecommerceItems[sku][1] = ""; + } + if(!isDefined(ecommerceItems[sku][2])) { + ecommerceItems[sku][2] = ""; + } + // Set price to zero + if(!isDefined(ecommerceItems[sku][3]) + || String(ecommerceItems[sku][3]).length === 0) { + ecommerceItems[sku][3] = 0; + } + // Set quantity to 1 + if(!isDefined(ecommerceItems[sku][4]) + || String(ecommerceItems[sku][4]).length === 0) { + ecommerceItems[sku][4] = 1; + } + items.push(ecommerceItems[sku]); + } + } + request += '&ec_items=' + encodeWrapper(JSON2.stringify(items)); + } + request = getRequest(request, configCustomData, 'ecommerce', lastEcommerceOrderTs); + sendRequest(request, configTrackerPause); + } + + function logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount) { + if(String(orderId).length + && isDefined(grandTotal)) { + logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount); + } + } + function logEcommerceCartUpdate(grandTotal) { + if(isDefined(grandTotal)) { + logEcommerce("", grandTotal, "", "", "", ""); + } + } + /* * Log the page view / visit */ @@ -1848,8 +1936,8 @@ var * To access specific data point, you should use the other functions getAttributionReferrer* and getAttributionCampaign* * * @return array Attribution array, Example use: - * 1) Call JSON2.stringify(piwikTracker.getAttributionInfo()) - * 2) Pass this json encoded string to the Tracking API (php or java client): setAttributionInfo() + * 1) Call JSON2.stringify(piwikTracker.getAttributionInfo()) + * 2) Pass this json encoded string to the Tracking API (php or java client): setAttributionInfo() */ getAttributionInfo: function() { return loadReferrerAttributionCookie(); @@ -2292,7 +2380,55 @@ var */ trackPageView: function (customTitle, customData) { logPageView(customTitle, customData); + }, + + /** + * Adds an item (product) that is in the current Cart or in the Ecommerce order. + * This function is called for every item (product) in the Cart or the Order. + * The only required parameter is sku. + * + * @param string sku (required) Item's SKU Code. This is the unique identifier for the product. + * @param string name (optional) Item's name + * @param string name (optional) Item's category + * @param float price (optional) Item's price. If not specified, will default to 0 + * @param float quantity (optional) Item's quantity. If not specified, will default to 1 + */ + addEcommerceItem: function(sku, name, category, price, quantity) { + if(sku.length) { + ecommerceItems[sku] = [ sku, name, category, price, quantity ]; + } + }, + + /** + * Tracks an Ecommerce order. + * If the Ecommerce order contains items (products), you must call first the addEcommerceItem() for each item in the order. + * All revenues (grandTotal, subTotal, tax, shipping, discount) will be individually summed and reported in Piwik reports. + * Parameters orderId and grandTotal are required. For others, you can set empty string "" if you don't need specify them. + * + * @param string|int orderId (required) Unique Order ID. + * This will be used to count this order only once in the event the order page is reloaded several times. + * orderId must be unique for each transaction, even on different days, or the transaction will not be recorded by Piwik. + * @param float grandTotal (required) Grand Total revenue of the transaction (including tax, shipping, etc.) + * @param float subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied) + * @param float tax (optional) Tax amount for this order + * @param float shipping (optional) Shipping amount for this order + * @param float discount (optional) Discounted amount in this order + */ + trackEcommerceOrder: function(orderId, grandTotal, subTotal, tax, shipping, discount) { + logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount); + }, + + /** + * Tracks a Cart Update (add item, remove item, update item). + * On every Cart update, you must call addEcommerceItem() for each item (product) in the cart, including the items that haven't been updated since the last cart update. + * Then you can call this function with the Cart grandTotal (typically the sum of all items' prices) + * + * @param float grandTotal (required) Items (products) amount in the Cart + */ + trackEcommerceCartUpdate: function(grandTotal) { + logEcommerceCartUpdate(grandTotal); } + }; } diff --git a/lang/en.php b/lang/en.php index 2d56104b313cb3f82679688a738434bc096b06a3..53e36429771fad1352f49b36d04bacaadf5d1d0d 100644 --- a/lang/en.php +++ b/lang/en.php @@ -20,6 +20,7 @@ $translations = array( 'General_Never' => 'Never', 'General_Required' => '%s required', 'General_NotValid' => '%s is not valid', + 'General_NotDefined' => '%s not defined', 'General_Id' => 'Id', 'General_Error' => 'Error', 'General_Warning' => 'Warning', @@ -48,8 +49,10 @@ $translations = array( 'General_VisitType' => 'Visitor type', 'General_DaysSinceLastVisit' => 'Days since last visit', 'General_DaysSinceFirstVisit' => 'Days since first visit', + 'General_DaysSinceLastEcommerceOrder' => 'Days since last Ecommerce order', 'General_NumberOfVisits' => 'Number of visits', 'General_VisitConvertedGoal' => 'Visit converted at least one Goal', + 'General_EcommerceVisitStatus' => 'Visit Ecommerce status at the end of the visit. For example, to select all visits that have made an Ecommerce order, the API request would contain %s', 'General_VisitConvertedNGoals' => 'Visit converted %s Goals', 'General_NewVisitor' => 'New Visitor', 'General_ReturningVisitor' => 'Returning Visitor', @@ -565,6 +568,9 @@ $translations = array( 'Goals_Pattern' => 'Pattern', 'Goals_ExceptionInvalidMatchingString' => 'If you choose \'exact match\', the matching string must be a URL starting with %s. For example, \'%s\'.', 'Goals_LearnMoreAboutGoalTrackingDocumentation' => 'Learn more about %s Tracking Goals in Piwik%s in the user documentation.', + 'Goals_ProductSKU' => 'Product SKU', + 'Goals_ProductName' => 'Product Name', + 'Goals_ProductCategory' => 'Product Category', 'Installation_PluginDescription' => 'Installation process of Piwik. The Installation is usually done once only. If the configuration file config/config.inc.php is deleted, the installation will start again.', 'Installation_Installation' => 'Installation', 'Installation_InstallationStatus' => 'Installation status', diff --git a/libs/PiwikTracker/PiwikTracker.php b/libs/PiwikTracker/PiwikTracker.php index b4ad4ba5f805f6dab259ded3304d8597d91384ef..a7f04e12546218f4e3c1a884e5d7b7b44c3e2aff 100644 --- a/libs/PiwikTracker/PiwikTracker.php +++ b/libs/PiwikTracker/PiwikTracker.php @@ -75,6 +75,7 @@ class PiwikTracker $this->forcedDatetime = false; $this->token_auth = false; $this->attributionInfo = false; + $this->ecommerceItems = array(); $this->requestCookie = ''; $this->idSite = $idSite; @@ -192,6 +193,9 @@ class PiwikTracker return $cookieDecoded[$id]; } + + + /** * Sets the Browser language. Used to guess visitor countries when GeoIP is not enabled * @@ -252,6 +256,139 @@ class PiwikTracker $url = $this->getUrlTrackAction($actionUrl, $actionType); return $this->sendRequest($url); } + + /** + * Adds an item in the Ecommerce order. + * This can be called before doTrackEcommerceOrder to define individual items in the order. + * SKU parameter is mandatory. Other parameters are optional, you can set all or only some to false or empty string. + * Ecommerce items added via this function are automatically cleared when doTrackEcommerceOrder or getUrlTrackEcommerceOrder is called. + * + * @param string $sku (required) SKU, Product identifier + * @param string $name (optional) Product name + * @param string $category (optional) Product category + * @param float|int $price (optional) Individual product price (supports integer and decimal prices) + * @param int $quantity (optional) Product quantity. If not specified, will default to 1 in the Reports + */ + public function addEcommerceItem($sku, $name = false, $category = false, $price = false, $quantity = false) + { + if(empty($sku)) + { + throw new Exception("You must specify a SKU for the Ecommerce item"); + } + $this->ecommerceItems[$sku] = array( $sku, $name, $category, $price, $quantity ); + } + + /** + * Tracks a Cart Update (add item, remove item, update item). + * On every Cart update, you must call addEcommerceItem() for each item (product) in the cart, including the items that haven't been updated since the last cart update. + * + * @param float $grandTotal Cart grandTotal (typically the sum of all items' prices) + */ + public function doTrackEcommerceCartUpdate($grandTotal) + { + $url = $this->getUrlTrackEcommerceCartUpdate($grandTotal); + return $this->sendRequest($url); + } + + /** + * Tracks an Ecommerce order. + * If the Ecommerce order contains items (products), you must call first the addEcommerceItem() for each item in the order. + * All revenues (grandTotal, subTotal, tax, shipping, discount) will be individually summed and reported in Piwik reports. + * Only parameters $orderId and $grandTotal are required. + * + * @param string|int $orderId (required) Unique Order ID. + * This will be used to count this order only once in the event the order page is reloaded several times. + * orderId must be unique for each transaction, even on different days, or the transaction will not be recorded by Piwik. + * @param float $grandTotal (required) Grand Total revenue of the transaction (including tax, shipping, etc.) + * @param float $subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied) + * @param float $tax (optional) Tax amount for this order + * @param float $shipping (optional) Shipping amount for this order + * @param float $discount (optional) Discounted amount in this order + */ + public function doTrackEcommerceOrder($orderId, $grandTotal, $subTotal = false, $tax = false, $shipping = false, $discount = false) + { + $url = $this->getUrlTrackEcommerceOrder($orderId, $grandTotal, $subTotal, $tax, $shipping, $discount); + return $this->sendRequest($url); + } + + /** + * Returns URL used to track Ecommerce Cart updates + * Calling this function will reinitializes the property ecommerceItems to empty array + * so items will have to be added again via addEcommerceItem() + * @ignore + */ + public function getUrlTrackEcommerceCartUpdate($grandTotal) + { + $url = $this->getUrlTrackEcommerce($grandTotal); + return $url; + } + + /** + * Returns URL used to track Ecommerce Orders + * Calling this function will reinitializes the property ecommerceItems to empty array + * so items will have to be added again via addEcommerceItem() + * @ignore + */ + public function getUrlTrackEcommerceOrder($orderId, $grandTotal, $subTotal = false, $tax = false, $shipping = false, $discount = false) + { + if(empty($orderId)) + { + throw new Exception("You must specifiy an orderId for the Ecommerce order"); + } + $url = $this->getUrlTrackEcommerce($grandTotal, $subTotal, $tax, $shipping, $discount); + $url .= '&ec_id=' . urlencode($orderId); + + return $url; + } + + /** + * Returns URL used to track Ecommerce orders + * Calling this function will reinitializes the property ecommerceItems to empty array + * so items will have to be added again via addEcommerceItem() + * @ignore + */ + protected function getUrlTrackEcommerce($grandTotal, $subTotal = false, $tax = false, $shipping = false, $discount = false) + { + if(!is_numeric($grandTotal)) + { + throw new Exception("You must specifiy a grandTotal for the Ecommerce order (or Cart update)"); + } + + $url = $this->getRequest( $this->idSite ); + $url .= '&idgoal=0'; + if(!empty($grandTotal)) + { + $url .= '&revenue='.$grandTotal; + } + if(!empty($subTotal)) + { + $url .= '&ec_st='.$subTotal; + } + if(!empty($tax)) + { + $url .= '&ec_tx='.$tax; + } + if(!empty($shipping)) + { + $url .= '&ec_sh='.$shipping; + } + if(!empty($discount)) + { + $url .= '&ec_dt='.$discount; + } + if(!empty($this->ecommerceItems)) + { + // Removing the SKU index in the array before JSON encoding + $items = array(); + foreach($this->ecommerceItems as $item) + { + $items[] = $item; + } + $this->ecommerceItems = array(); + $url .= '&ec_items='. urlencode(json_encode($items)); + } + return $url; + } /** * @see doTrackPageView() @@ -443,6 +580,15 @@ class PiwikTracker $this->hasCookies = $bool ; } + /** + * Will append a custom string at the end of the Tracking request. + * @param string $string + */ + public function setDebugStringAppend( $string ) + { + $this->DEBUG_APPEND_URL = $string; + } + /** * Sets visitor browser supported plugins * diff --git a/piwik.js b/piwik.js index 5872246f71b99c4db512eab05dcd0393309019e8..7f0cbabdc6c6da64fee4f44cc6051f34bd496ba4 100644 --- a/piwik.js +++ b/piwik.js @@ -14,13 +14,14 @@ return typeof f==="function"?m({"":n},""):n}throw new SyntaxError("JSON.parse")} k()})}else{if(d.attachEvent){d.attachEvent("onreadystatechange",function i(){if(d.readyState==="complete"){d.detachEvent("onreadystatechange",i);k()}});if(d.documentElement.doScroll&&H===H.top){(function i(){if(!h){try{d.documentElement.doScroll("left")}catch(K){setTimeout(i,0);return}k()}}())}}}if((new RegExp("WebKit")).test(j.userAgent)){J=setInterval(function(){if(h||/loaded|complete/.test(d.readyState)){clearInterval(J);k()}},10)}t(H,"load",k,false)}function f(){var i="";try{i=H.top.document.referrer}catch(K){if(H.parent){try{i=H.parent.document.referrer}catch(J){i=""}}}if(i===""){i=d.referrer}return i}function A(i){var K=new RegExp("^([a-z]+):"),J=K.exec(i);return J?J[1]:null}function y(i){var K=new RegExp("^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)"),J=K.exec(i);return J?J[1]:i}function p(K,J){var N=new RegExp("^(?:https?|ftp)(?::/*(?:[^?]+)[?])([^#]+)"),M=N.exec(K),L=new RegExp("(?:^|&)"+J+"=([^&]*)"),i=M?L.exec(M[1]):0;return i?I(i[1]):""}function s(O,L,K,N,J,M){var i;if(K){i=new Date(); i.setTime(i.getTime()+K)}d.cookie=O+"="+e(L)+(K?";expires="+i.toGMTString():"")+";path="+(N?N:"/")+(J?";domain="+J:"")+(M?";secure":"")}function F(K){var i=new RegExp("(^|;)[ ]*"+K+"=([^;]*)"),J=i.exec(d.cookie);return J?I(J[2]):0}function r(i){return unescape(e(i))}function u(Z){var L=function(W,i){return(W<<i)|(W>>>(32-i))},aa=function(ag){var af="",ae,W;for(ae=7;ae>=0;ae--){W=(ag>>>(ae*4))&15;af+=W.toString(16)}return af},O,ac,ab,K=[],S=1732584193,Q=4023233417,P=2562383102,N=271733878,M=3285377520,Y,X,V,U,T,ad,J,R=[];Z=r(Z);J=Z.length;for(ac=0;ac<J-3;ac+=4){ab=Z.charCodeAt(ac)<<24|Z.charCodeAt(ac+1)<<16|Z.charCodeAt(ac+2)<<8|Z.charCodeAt(ac+3);R.push(ab)}switch(J&3){case 0:ac=2147483648;break;case 1:ac=Z.charCodeAt(J-1)<<24|8388608;break;case 2:ac=Z.charCodeAt(J-2)<<24|Z.charCodeAt(J-1)<<16|32768;break;case 3:ac=Z.charCodeAt(J-3)<<24|Z.charCodeAt(J-2)<<16|Z.charCodeAt(J-1)<<8|128;break}R.push(ac);while((R.length&15)!==14){R.push(0)}R.push(J>>>29);R.push((J<<3)&4294967295);for(O=0;O<R.length; O+=16){for(ac=0;ac<16;ac++){K[ac]=R[O+ac]}for(ac=16;ac<=79;ac++){K[ac]=L(K[ac-3]^K[ac-8]^K[ac-14]^K[ac-16],1)}Y=S;X=Q;V=P;U=N;T=M;for(ac=0;ac<=19;ac++){ad=(L(Y,5)+((X&V)|(~X&U))+T+K[ac]+1518500249)&4294967295;T=U;U=V;V=L(X,30);X=Y;Y=ad}for(ac=20;ac<=39;ac++){ad=(L(Y,5)+(X^V^U)+T+K[ac]+1859775393)&4294967295;T=U;U=V;V=L(X,30);X=Y;Y=ad}for(ac=40;ac<=59;ac++){ad=(L(Y,5)+((X&V)|(X&U)|(V&U))+T+K[ac]+2400959708)&4294967295;T=U;U=V;V=L(X,30);X=Y;Y=ad}for(ac=60;ac<=79;ac++){ad=(L(Y,5)+(X^V^U)+T+K[ac]+3395469782)&4294967295;T=U;U=V;V=L(X,30);X=Y;Y=ad}S=(S+Y)&4294967295;Q=(Q+X)&4294967295;P=(P+V)&4294967295;N=(N+U)&4294967295;M=(M+T)&4294967295}ad=aa(S)+aa(Q)+aa(P)+aa(N)+aa(M);return ad.toLowerCase()}function o(K,i,J){if(K==="translate.googleusercontent.com"){if(J===""){J=i}i=p(i,"u");K=y(i)}else{if(K==="cc.bingj.com"||K==="webcache.googleusercontent.com"||K.slice(0,5)==="74.6."){i=d.links[0].href;K=y(i)}}return[K,i,J]}function l(J){var i=J.length;if(J.charAt(--i)==="."){J=J.slice(0,i)}if(J.slice(0,2)==="*."){J=J.slice(1) -}return J}function E(aF,aD){var ao=o(d.domain,H.location.href,f()),aa=l(ao[0]),W=ao[1],aG=ao[2],L="GET",ad=aF||"",aZ=aD||"",aR,aY=d.title,aj="7z|aac|ar[cj]|as[fx]|avi|bin|csv|deb|dmg|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|ms[ip]|od[bfgpst]|og[gv]|pdf|phps|png|ppt|qtm?|ra[mr]?|rpm|sea|sit|tar|t?bz2?|tgz|torrent|txt|wav|wm[av]|wpd||xls|xml|z|zip",aH=[aa],P=[],aI=[],aN=[],ac=500,K,am,an,aA,at=["pk_campaign","piwik_campaign","utm_campaign","utm_source","utm_medium"],aC=["pk_kwd","piwik_kwd","utm_term"],aJ="_pk_",S,aE,M,ax,a0=63072000000,ag=1800000,ab=15768000000,aW=d.location.protocol==="https",aO=false,U=100,aL=5,aq={},aw=false,T=false,Z,aV,au,aQ=u,aB,al;function aS(a1){var a2;if(an){a2=new RegExp("#.*");return a1.replace(a2,"")}return a1}function ai(a3,a1){var a4=A(a1),a2;if(a4){return a1}if(a1.slice(0,1)==="/"){return A(a3)+"://"+y(a3)+a1}a3=aS(a3);if((a2=a3.indexOf("?"))>=0){a3=a3.slice(0,a2)}if((a2=a3.lastIndexOf("/"))!==a3.length-1){a3=a3.slice(0,a2+1)}return a3+a1 -}function av(a4){var a2,a1,a3;for(a2=0;a2<aH.length;a2++){a1=l(aH[a2].toLowerCase());if(a4===a1){return true}if(a1.slice(0,1)==="."){if(a4===a1.slice(1)){return true}a3=a4.length-a1.length;if((a3>0)&&(a4.slice(a3)===a1)){return true}}}return false}function i(a1){var a2=new Image(1,1);a2.onLoad=function(){};a2.src=ad+(ad.indexOf("?")<0?"?":"&")+a1}function Y(a1){try{var a3=H.XDomainRequest?new H.XDomainRequest():H.XMLHttpRequest?new H.XMLHttpRequest():H.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):null;a3.open("POST",ad,true);a3.onreadystatechange=function(){if(this.readyState===4&&this.status!==200){i(a1)}};a3.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");a3.send(a1)}catch(a2){i(a1)}}function aU(a3,a2){var a1=new Date();if(!M){if(L==="POST"){Y(a3)}else{i(a3)}m=a1.getTime()+a2}}function Q(a1){return aJ+a1+"."+aZ+"."+aB}function az(){var a1=Q("testcookie");if(!b(j.cookieEnabled)){s(a1,"1");return F(a1)==="1"?"1":"0"}return j.cookieEnabled?"1":"0" -}function ak(){aB=aQ((S||aa)+(aE||"/")).slice(0,4)}function X(){var a2=Q("cvar"),a1=F(a2);if(a1.length){a1=JSON2.parse(a1);if(n(a1)){return a1}}return{}}function aK(){if(aO===false){aO=X()}}function R(){var a1=new Date();Z=a1.getTime()}function N(a5,a2,a1,a4,a3){s(Q("id"),a5+"."+a2+"."+a1+"."+a4+"."+a3,a0,aE,S,aW)}function O(){var a2=new Date(),a1=Math.round(a2.getTime()/1000),a4=F(Q("id")),a3;if(a4){a3=a4.split(".");a3.unshift("0")}else{if(!al){al=aQ((j.userAgent||"")+(j.platform||"")+JSON2.stringify(aq)+a1).slice(0,16)}a3=["1",al,a1,0,a1,""]}return a3}function aM(){var a1=F(Q("ref"));if(a1.length){try{a1=JSON2.parse(a1);if(n(a1)){return a1}}catch(a2){}}return["","",0,""]}function ap(a3,bn,bo){var bl,a2=new Date(),a9=Math.round(a2.getTime()/1000),bq,bm,a5,bf,bi,a8,a6,bk,a4=1024,br,bc,bh=aO,be=Q("id"),ba=Q("ses"),bb=Q("ref"),bs=Q("cvar"),bg=O(),bd=F(ba),bj=aM(),bp=aR||W,a7,a1;if(M){s(be,"",-1,aE,S);s(ba,"",-1,aE,S);s(bs,"",-1,aE,S);s(bb,"",-1,aE,S);return""}bq=bg[0];bm=bg[1];bf=bg[2];a5=bg[3]; -bi=bg[4];a8=bg[5];a7=bj[0];a1=bj[1];a6=bj[2];bk=bj[3];if(!bd){a5++;a8=bi;if(!ax||!a7.length){for(bl in at){if(Object.prototype.hasOwnProperty.call(at,bl)){a7=p(bp,at[bl]);if(a7.length){break}}}for(bl in aC){if(Object.prototype.hasOwnProperty.call(aC,bl)){a1=p(bp,aC[bl]);if(a1.length){break}}}}br=y(aG);bc=bk.length?y(bk):"";if(br.length&&!av(br)&&(!ax||!bc.length||av(bc))){bk=aG}if(bk.length||a7.length){a6=a9;bj=[a7,a1,a6,aS(bk.slice(0,a4))];s(bb,JSON2.stringify(bj),ab,aE,S,aW)}}a3+="&idsite="+aZ+"&rec=1&rand="+Math.random()+"&h="+a2.getHours()+"&m="+a2.getMinutes()+"&s="+a2.getSeconds()+"&url="+e(aS(bp))+"&urlref="+e(aS(aG))+"&_id="+bm+"&_idts="+bf+"&_idvc="+a5+"&_idn="+bq+"&_rcn="+e(a7)+"&_rck="+e(a1)+"&_refts="+a6+"&_viewts="+a8+"&_ref="+e(aS(bk.slice(0,a4)));for(bl in aq){if(Object.prototype.hasOwnProperty.call(aq,bl)){a3+="&"+bl+"="+aq[bl]}}if(bn){a3+="&data="+e(JSON2.stringify(bn))}else{if(aA){a3+="&data="+e(JSON2.stringify(aA))}}if(aO){a3+="&_cvar="+e(JSON2.stringify(aO));for(bl in bh){if(Object.prototype.hasOwnProperty.call(bh,bl)){if(aO[bl][0]===""||aO[bl][1]===""){delete aO[bl] -}}}s(bs,JSON2.stringify(aO),ag,aE,S,aW)}N(bm,bf,a5,a9,a8);s(ba,"*",ag,aE,S,aW);a3+=g(bo);return a3}function J(a4,a5){var a1=new Date(),a3=ap("action_name="+e(a4||aY),a5,"log");aU(a3,ac);if(K&&am&&!T){T=true;t(d,"click",R);t(d,"mouseup",R);t(d,"mousedown",R);t(d,"mousemove",R);t(d,"mousewheel",R);t(H,"DOMMouseScroll",R);t(H,"scroll",R);t(d,"keypress",R);t(d,"keydown",R);t(d,"keyup",R);t(H,"resize",R);t(H,"focus",R);t(H,"blur",R);Z=a1.getTime();setTimeout(function a2(){var a6=new Date(),a7;if((Z+am)>a6.getTime()){if(K<a6.getTime()){a7=ap("ping=1",a5,"ping");aU(a7,ac)}setTimeout(a2,am)}},am)}}function aT(a1,a4,a3){var a2=ap("idgoal="+a1+(a4?"&revenue="+a4:""),a3,"goal");aU(a2,ac)}function ah(a2,a1,a4){var a3=ap(a1+"="+e(aS(a2)),a4,"link");aU(a3,ac)}function ay(a3,a2){var a4,a1="(^| )(piwik[_-]"+a2;if(a3){for(a4=0;a4<a3.length;a4++){a1+="|"+a3[a4]}}a1+=")( |$)";return new RegExp(a1)}function aX(a4,a1,a5){if(!a5){return"link"}var a3=ay(aI,"download"),a2=ay(aN,"link"),a6=new RegExp("\\.("+aj+")([?&#]|$)","i"); -return a2.test(a4)?"link":(a3.test(a4)||a6.test(a1)?"download":0)}function V(a6){var a4,a2,a1;while(!!(a4=a6.parentNode)&&((a2=a6.tagName)!=="A"&&a2!=="AREA")){a6=a4}if(b(a6.href)){var a7=a6.hostname||y(a6.href),a8=a7.toLowerCase(),a3=a6.href.replace(a7,a8),a5=new RegExp("^(javascript|vbscript|jscript|mocha|livescript|ecmascript):","i");if(!a5.test(a3)){a1=aX(a6.className,a3,av(a8));if(a1){ah(a3,a1)}}}}function ae(a1){var a2,a3;a1=a1||H.event;a2=a1.which||a1.button;a3=a1.target||a1.srcElement;if(a1.type==="click"){if(a3){V(a3)}}else{if(a1.type==="mousedown"){if((a2===1||a2===2)&&a3){aV=a2;au=a3}else{aV=au=null}}else{if(a1.type==="mouseup"){if(a2===aV&&a3===au){V(a3)}aV=au=null}}}}function aP(a2,a1){if(a1){t(a2,"mouseup",ae,false);t(a2,"mousedown",ae,false)}else{t(a2,"click",ae,false)}}function ar(a2){if(!aw){aw=true;var a3,a1=ay(P,"ignore"),a4=d.links;if(a4){for(a3=0;a3<a4.length;a3++){if(!a1.test(a4[a3].className)){aP(a4[a3],a2)}}}}}function af(){var a1,a2,a3={pdf:"application/pdf",qt:"video/quicktime",realp:"audio/x-pn-realaudio-plugin",wma:"application/x-mplayer2",dir:"application/x-director",fla:"application/x-shockwave-flash",java:"application/x-java-vm",gears:"application/x-googlegears",ag:"application/x-silverlight"}; -if(j.mimeTypes&&j.mimeTypes.length){for(a1 in a3){if(Object.prototype.hasOwnProperty.call(a3,a1)){a2=j.mimeTypes[a3[a1]];aq[a1]=(a2&&a2.enabledPlugin)?"1":"0"}}}if(typeof navigator.javaEnabled!=="unknown"&&b(j.javaEnabled)&&j.javaEnabled()){aq.java="1"}if(a(H.GearsFactory)){aq.gears="1"}aq.res=v.width+"x"+v.height;aq.cookie=az()}af();ak();return{getVisitorId:function(){return(O())[1]},getVisitorInfo:function(){return O()},getAttributionInfo:function(){return aM()},getAttributionCampaignName:function(){return aM()[0]},getAttributionCampaignKeyword:function(){return aM()[1]},getAttributionReferrerTimestamp:function(){return aM()[2]},getAttributionReferrerUrl:function(){return aM()[3]},setTrackerUrl:function(a1){ad=a1},setSiteId:function(a1){aZ=a1},setCustomData:function(a1,a2){if(n(a1)){aA=a1}else{if(!aA){aA=[]}aA[a1]=a2}},getCustomData:function(){return aA},setCustomVariable:function(a2,a1,a3){aK();if(a2>0&&a2<=aL){aO[a2]=[a1.slice(0,U),a3.slice(0,U)]}},getCustomVariable:function(a2){var a1; -aK();a1=aO[a2];if(a1&&a1[0]===""){return}return aO[a2]},deleteCustomVariable:function(a1){if(this.getCustomVariable(a1)){this.setCustomVariable(a1,"","")}},setLinkTrackingTimer:function(a1){ac=a1},setDownloadExtensions:function(a1){aj=a1},addDownloadExtensions:function(a1){aj+="|"+a1},setDomains:function(a1){aH=q(a1)?[a1]:a1;aH.push(aa)},setIgnoreClasses:function(a1){P=q(a1)?[a1]:a1},setRequestMethod:function(a1){L=a1||"GET"},setReferrerUrl:function(a1){aG=a1},setCustomUrl:function(a1){aR=ai(W,a1)},setDocumentTitle:function(a1){aY=a1},setDownloadClasses:function(a1){aI=q(a1)?[a1]:a1},setLinkClasses:function(a1){aN=q(a1)?[a1]:a1},setCampaignNameKey:function(a1){at=q(a1)?[a1]:a1},setCampaignKeywordKey:function(a1){aC=q(a1)?[a1]:a1},discardHashTag:function(a1){an=a1},setCookieNamePrefix:function(a1){aJ=a1;aO=X()},setCookieDomain:function(a1){S=l(a1);ak()},setCookiePath:function(a1){aE=a1;ak()},setVisitorCookieTimeout:function(a1){a0=a1*1000},setSessionCookieTimeout:function(a1){ag=a1*1000},setReferralCookieTimeout:function(a1){ab=a1*1000 -},setConversionAttributionFirstReferrer:function(a1){ax=a1},setDoNotTrack:function(a1){M=a1&&j.doNotTrack},addListener:function(a2,a1){aP(a2,a1)},enableLinkTracking:function(a1){if(h){ar(a1)}else{C.push(function(){ar(a1)})}},setHeartBeatTimer:function(a3,a2){var a1=new Date();K=a1.getTime()+a3*1000;am=a2*1000},killFrame:function(){if(H.location!==H.top.location){H.top.location=H.location}},redirectFile:function(a1){if(H.location.protocol==="file:"){H.location=a1}},trackGoal:function(a1,a3,a2){aT(a1,a3,a2)},trackLink:function(a2,a1,a3){ah(a2,a1,a3)},trackPageView:function(a1,a2){J(a1,a2)}}}function c(){return{push:z}}t(H,"beforeunload",B,false);x();G=new E();for(D=0;D<_paq.length;D++){z(_paq[D])}_paq=new c();return{addPlugin:function(i,J){w[i]=J},getTracker:function(i,J){return new E(i,J)},getAsyncTracker:function(){return G}}}()),piwik_track,piwik_log=function(b,f,d,g){function a(h){try{return eval("piwik_"+h)}catch(i){}return}var c,e=Piwik.getTracker(d,f);e.setDocumentTitle(b); -e.setCustomData(g);if(!!(c=a("tracker_pause"))){e.setLinkTrackingTimer(c)}if(!!(c=a("download_extensions"))){e.setDownloadExtensions(c)}if(!!(c=a("hosts_alias"))){e.setDomains(c)}if(!!(c=a("ignore_classes"))){e.setIgnoreClasses(c)}e.trackPageView();if((a("install_tracker"))){piwik_track=function(i,k,j,h){e.setSiteId(k);e.setTrackerUrl(j);e.trackLink(i,h)};e.enableLinkTracking()}}; \ No newline at end of file +}return J}function E(ab,ax){var M=o(d.domain,H.location.href,f()),aP=l(M[0]),a2=M[1],aD=M[2],aB="GET",L=ab||"",aT=ax||"",ao,ag=d.title,ai="7z|aac|ar[cj]|as[fx]|avi|bin|csv|deb|dmg|doc|exe|flv|gif|gz|gzip|hqx|jar|jpe?g|js|mp(2|3|4|e?g)|mov(ie)?|ms[ip]|od[bfgpst]|og[gv]|pdf|phps|png|ppt|qtm?|ra[mr]?|rpm|sea|sit|tar|t?bz2?|tgz|torrent|txt|wav|wm[av]|wpd||xls|xml|z|zip",az=[aP],P=[],at=[],aa=[],ay=500,Q,ac,R,S,ak=["pk_campaign","piwik_campaign","utm_campaign","utm_source","utm_medium"],af=["pk_kwd","piwik_kwd","utm_term"],a0="_pk_",U,a1,aV,an,Y=63072000000,Z=1800000,ap=15768000000,X=d.location.protocol==="https",O=false,aW=100,ae=5,aJ={},aU={},aG=false,aE=false,aC,au,V,aj=u,aF,am;function aX(a5){var a6;if(R){a6=new RegExp("#.*");return a5.replace(a6,"")}return a5}function aO(a7,a5){var a8=A(a5),a6;if(a8){return a5}if(a5.slice(0,1)==="/"){return A(a7)+"://"+y(a7)+a5}a7=aX(a7);if((a6=a7.indexOf("?"))>=0){a7=a7.slice(0,a6)}if((a6=a7.lastIndexOf("/"))!==a7.length-1){a7=a7.slice(0,a6+1)}return a7+a5 +}function aA(a8){var a6,a5,a7;for(a6=0;a6<az.length;a6++){a5=l(az[a6].toLowerCase());if(a8===a5){return true}if(a5.slice(0,1)==="."){if(a8===a5.slice(1)){return true}a7=a8.length-a5.length;if((a7>0)&&(a8.slice(a7)===a5)){return true}}}return false}function a4(a5){var a6=new Image(1,1);a6.onLoad=function(){};a6.src=L+(L.indexOf("?")<0?"?":"&")+a5}function aL(a5){try{var a7=H.XDomainRequest?new H.XDomainRequest():H.XMLHttpRequest?new H.XMLHttpRequest():H.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):null;a7.open("POST",L,true);a7.onreadystatechange=function(){if(this.readyState===4&&this.status!==200){a4(a5)}};a7.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");a7.send(a5)}catch(a6){a4(a5)}}function al(a7,a6){var a5=new Date();if(!aV){if(aB==="POST"){aL(a7)}else{a4(a7)}m=a5.getTime()+a6}}function aK(a5){return a0+a5+"."+aT+"."+aF}function N(){var a5=aK("testcookie");if(!b(j.cookieEnabled)){s(a5,"1");return F(a5)==="1"?"1":"0"}return j.cookieEnabled?"1":"0" +}function av(){aF=aj((U||aP)+(a1||"/")).slice(0,4)}function W(){var a6=aK("cvar"),a5=F(a6);if(a5.length){a5=JSON2.parse(a5);if(n(a5)){return a5}}return{}}function K(){if(O===false){O=W()}}function aS(){var a5=new Date();aC=a5.getTime()}function T(a9,a6,a5,a8,a7,ba){s(aK("id"),a9+"."+a6+"."+a5+"."+a8+"."+a7+"."+ba,Y,a1,U,X)}function J(){var a6=new Date(),a5=Math.round(a6.getTime()/1000),a8=F(aK("id")),a7;if(a8){a7=a8.split(".");a7.unshift("0")}else{if(!am){am=aj((j.userAgent||"")+(j.platform||"")+JSON2.stringify(aU)+a5).slice(0,16)}a7=["1",am,a5,0,a5,"",""]}return a7}function i(){var a5=F(aK("ref"));if(a5.length){try{a5=JSON2.parse(a5);if(n(a5)){return a5}}catch(a6){}}return["","",0,""]}function ah(a7,bu,bv,a8){var bs,a6=new Date(),be=Math.round(a6.getTime()/1000),bx,bt,ba,bl,bp,bd,bn,bb,br,a9=1024,by,bh,bo=O,bj=aK("id"),bf=aK("ses"),bg=aK("ref"),bz=aK("cvar"),bm=J(),bi=F(bf),bq=i(),bw=ao||a2,bc,a5;if(aV){s(bj,"",-1,a1,U);s(bf,"",-1,a1,U);s(bz,"",-1,a1,U);s(bg,"",-1,a1,U);return""}bx=bm[0]; +bt=bm[1];bl=bm[2];ba=bm[3];bp=bm[4];bd=bm[5];if(!b(bm[6])){bm[6]=""}bn=bm[6];if(!b(a8)){a8=""}bc=bq[0];a5=bq[1];bb=bq[2];br=bq[3];if(!bi){ba++;bd=bp;if(!an||!bc.length){for(bs in ak){if(Object.prototype.hasOwnProperty.call(ak,bs)){bc=p(bw,ak[bs]);if(bc.length){break}}}for(bs in af){if(Object.prototype.hasOwnProperty.call(af,bs)){a5=p(bw,af[bs]);if(a5.length){break}}}}by=y(aD);bh=br.length?y(br):"";if(by.length&&!aA(by)&&(!an||!bh.length||aA(bh))){br=aD}if(br.length||bc.length){bb=be;bq=[bc,a5,bb,aX(br.slice(0,a9))];s(bg,JSON2.stringify(bq),ap,a1,U,X)}}a7+="&idsite="+aT+"&rec=1&r="+String(Math.random()).slice(2,8)+"&h="+a6.getHours()+"&m="+a6.getMinutes()+"&s="+a6.getSeconds()+"&url="+e(aX(bw))+(aD.length?"&urlref="+e(aX(aD)):"")+"&_id="+bt+"&_idts="+bl+"&_idvc="+ba+"&_idn="+bx+(bc.length?"&_rcn="+e(bc):"")+(a5.length?"&_rck="+e(a5):"")+"&_refts="+bb+"&_viewts="+bd+(String(bn).length?"&_ects="+bn:"")+(String(br).length?"&_ref="+e(aX(br.slice(0,a9))):"");for(bs in aU){if(Object.prototype.hasOwnProperty.call(aU,bs)){a7+="&"+bs+"="+aU[bs] +}}if(bu){a7+="&data="+e(JSON2.stringify(bu))}else{if(S){a7+="&data="+e(JSON2.stringify(S))}}if(O){var bk=JSON2.stringify(O);if(bk.length>2){a7+="&_cvar="+e(bk)}for(bs in bo){if(Object.prototype.hasOwnProperty.call(bo,bs)){if(O[bs][0]===""||O[bs][1]===""){delete O[bs]}}}s(bz,JSON2.stringify(O),Z,a1,U,X)}T(bt,bl,ba,be,bd,b(a8)&&String(a8).length?a8:bn);s(bf,"*",Z,a1,U,X);a7+=g(bv);return a7}function aN(a8,a7,bc,a9,a5,bf){var ba="idgoal=0",bb,a6=new Date(),bd=[],be;if(String(a8).length){ba+="&ec_id="+e(a8);bb=Math.round(a6.getTime()/1000)}ba+="&revenue="+a7;if(String(bc).length){ba+="&ec_st="+bc}if(String(a9).length){ba+="&ec_tx="+a9}if(String(a5).length){ba+="&ec_sh="+a5}if(String(bf).length){ba+="&ec_dt="+bf}if(aJ){for(be in aJ){if(Object.prototype.hasOwnProperty.call(aJ,be)){if(!b(aJ[be][1])){aJ[be][1]=""}if(!b(aJ[be][2])){aJ[be][2]=""}if(!b(aJ[be][3])||String(aJ[be][3]).length===0){aJ[be][3]=0}if(!b(aJ[be][4])||String(aJ[be][4]).length===0){aJ[be][4]=1}bd.push(aJ[be])}}ba+="&ec_items="+e(JSON2.stringify(bd)) +}ba=ah(ba,S,"ecommerce",bb);al(ba,ay)}function aM(a5,a9,a8,a7,a6,ba){if(String(a5).length&&b(a9)){aN(a5,a9,a8,a7,a6,ba)}}function aZ(a5){if(b(a5)){aN("",a5,"","","","")}}function ar(a8,a9){var a5=new Date(),a7=ah("action_name="+e(a8||ag),a9,"log");al(a7,ay);if(Q&&ac&&!aE){aE=true;t(d,"click",aS);t(d,"mouseup",aS);t(d,"mousedown",aS);t(d,"mousemove",aS);t(d,"mousewheel",aS);t(H,"DOMMouseScroll",aS);t(H,"scroll",aS);t(d,"keypress",aS);t(d,"keydown",aS);t(d,"keyup",aS);t(H,"resize",aS);t(H,"focus",aS);t(H,"blur",aS);aC=a5.getTime();setTimeout(function a6(){var ba=new Date(),bb;if((aC+ac)>ba.getTime()){if(Q<ba.getTime()){bb=ah("ping=1",a9,"ping");al(bb,ay)}setTimeout(a6,ac)}},ac)}}function aw(a5,a8,a7){var a6=ah("idgoal="+a5+(a8?"&revenue="+a8:""),a7,"goal");al(a6,ay)}function aR(a6,a5,a8){var a7=ah(a5+"="+e(aX(a6)),a8,"link");al(a7,ay)}function ad(a7,a6){var a8,a5="(^| )(piwik[_-]"+a6;if(a7){for(a8=0;a8<a7.length;a8++){a5+="|"+a7[a8]}}a5+=")( |$)";return new RegExp(a5)}function aQ(a8,a5,a9){if(!a9){return"link" +}var a7=ad(at,"download"),a6=ad(aa,"link"),ba=new RegExp("\\.("+ai+")([?&#]|$)","i");return a6.test(a8)?"link":(a7.test(a8)||ba.test(a5)?"download":0)}function aI(ba){var a8,a6,a5;while(!!(a8=ba.parentNode)&&((a6=ba.tagName)!=="A"&&a6!=="AREA")){ba=a8}if(b(ba.href)){var bb=ba.hostname||y(ba.href),bc=bb.toLowerCase(),a7=ba.href.replace(bb,bc),a9=new RegExp("^(javascript|vbscript|jscript|mocha|livescript|ecmascript):","i");if(!a9.test(a7)){a5=aQ(ba.className,a7,aA(bc));if(a5){aR(a7,a5)}}}}function a3(a5){var a6,a7;a5=a5||H.event;a6=a5.which||a5.button;a7=a5.target||a5.srcElement;if(a5.type==="click"){if(a7){aI(a7)}}else{if(a5.type==="mousedown"){if((a6===1||a6===2)&&a7){au=a6;V=a7}else{au=V=null}}else{if(a5.type==="mouseup"){if(a6===au&&a7===V){aI(a7)}au=V=null}}}}function aH(a6,a5){if(a5){t(a6,"mouseup",a3,false);t(a6,"mousedown",a3,false)}else{t(a6,"click",a3,false)}}function aq(a6){if(!aG){aG=true;var a7,a5=ad(P,"ignore"),a8=d.links;if(a8){for(a7=0;a7<a8.length;a7++){if(!a5.test(a8[a7].className)){aH(a8[a7],a6) +}}}}}function aY(){var a5,a6,a7={pdf:"application/pdf",qt:"video/quicktime",realp:"audio/x-pn-realaudio-plugin",wma:"application/x-mplayer2",dir:"application/x-director",fla:"application/x-shockwave-flash",java:"application/x-java-vm",gears:"application/x-googlegears",ag:"application/x-silverlight"};if(j.mimeTypes&&j.mimeTypes.length){for(a5 in a7){if(Object.prototype.hasOwnProperty.call(a7,a5)){a6=j.mimeTypes[a7[a5]];aU[a5]=(a6&&a6.enabledPlugin)?"1":"0"}}}if(typeof navigator.javaEnabled!=="unknown"&&b(j.javaEnabled)&&j.javaEnabled()){aU.java="1"}if(a(H.GearsFactory)){aU.gears="1"}aU.res=v.width+"x"+v.height;aU.cookie=N()}aY();av();return{getVisitorId:function(){return(J())[1]},getVisitorInfo:function(){return J()},getAttributionInfo:function(){return i()},getAttributionCampaignName:function(){return i()[0]},getAttributionCampaignKeyword:function(){return i()[1]},getAttributionReferrerTimestamp:function(){return i()[2]},getAttributionReferrerUrl:function(){return i()[3]},setTrackerUrl:function(a5){L=a5 +},setSiteId:function(a5){aT=a5},setCustomData:function(a5,a6){if(n(a5)){S=a5}else{if(!S){S=[]}S[a5]=a6}},getCustomData:function(){return S},setCustomVariable:function(a6,a5,a7){K();if(a6>0&&a6<=ae){O[a6]=[a5.slice(0,aW),a7.slice(0,aW)]}},getCustomVariable:function(a6){var a5;K();a5=O[a6];if(a5&&a5[0]===""){return}return O[a6]},deleteCustomVariable:function(a5){if(this.getCustomVariable(a5)){this.setCustomVariable(a5,"","")}},setLinkTrackingTimer:function(a5){ay=a5},setDownloadExtensions:function(a5){ai=a5},addDownloadExtensions:function(a5){ai+="|"+a5},setDomains:function(a5){az=q(a5)?[a5]:a5;az.push(aP)},setIgnoreClasses:function(a5){P=q(a5)?[a5]:a5},setRequestMethod:function(a5){aB=a5||"GET"},setReferrerUrl:function(a5){aD=a5},setCustomUrl:function(a5){ao=aO(a2,a5)},setDocumentTitle:function(a5){ag=a5},setDownloadClasses:function(a5){at=q(a5)?[a5]:a5},setLinkClasses:function(a5){aa=q(a5)?[a5]:a5},setCampaignNameKey:function(a5){ak=q(a5)?[a5]:a5},setCampaignKeywordKey:function(a5){af=q(a5)?[a5]:a5 +},discardHashTag:function(a5){R=a5},setCookieNamePrefix:function(a5){a0=a5;O=W()},setCookieDomain:function(a5){U=l(a5);av()},setCookiePath:function(a5){a1=a5;av()},setVisitorCookieTimeout:function(a5){Y=a5*1000},setSessionCookieTimeout:function(a5){Z=a5*1000},setReferralCookieTimeout:function(a5){ap=a5*1000},setConversionAttributionFirstReferrer:function(a5){an=a5},setDoNotTrack:function(a5){aV=a5&&j.doNotTrack},addListener:function(a6,a5){aH(a6,a5)},enableLinkTracking:function(a5){if(h){aq(a5)}else{C.push(function(){aq(a5)})}},setHeartBeatTimer:function(a7,a6){var a5=new Date();Q=a5.getTime()+a7*1000;ac=a6*1000},killFrame:function(){if(H.location!==H.top.location){H.top.location=H.location}},redirectFile:function(a5){if(H.location.protocol==="file:"){H.location=a5}},trackGoal:function(a5,a7,a6){aw(a5,a7,a6)},trackLink:function(a6,a5,a7){aR(a6,a5,a7)},trackPageView:function(a5,a6){ar(a5,a6)},addEcommerceItem:function(a9,a5,a7,a6,a8){if(a9.length){aJ[a9]=[a9,a5,a7,a6,a8]}},trackEcommerceOrder:function(a5,a9,a8,a7,a6,ba){aM(a5,a9,a8,a7,a6,ba) +},trackEcommerceCartUpdate:function(a5){aZ(a5)}}}function c(){return{push:z}}t(H,"beforeunload",B,false);x();G=new E();for(D=0;D<_paq.length;D++){z(_paq[D])}_paq=new c();return{addPlugin:function(i,J){w[i]=J},getTracker:function(i,J){return new E(i,J)},getAsyncTracker:function(){return G}}}()),piwik_track,piwik_log=function(b,f,d,g){function a(h){try{return eval("piwik_"+h)}catch(i){}return}var c,e=Piwik.getTracker(d,f);e.setDocumentTitle(b);e.setCustomData(g);if(!!(c=a("tracker_pause"))){e.setLinkTrackingTimer(c)}if(!!(c=a("download_extensions"))){e.setDownloadExtensions(c)}if(!!(c=a("hosts_alias"))){e.setDomains(c)}if(!!(c=a("ignore_classes"))){e.setIgnoreClasses(c)}e.trackPageView();if((a("install_tracker"))){piwik_track=function(i,k,j,h){e.setSiteId(k);e.setTrackerUrl(j);e.trackLink(i,h)};e.enableLinkTracking()}}; \ No newline at end of file diff --git a/plugins/API/API.php b/plugins/API/API.php index b46f1328a00941b60f081f47f232b6fa4bbbe9d4..93b46a2e25853ca898ef265d7d34fa2682acfd6a 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -201,6 +201,25 @@ class Piwik_API_API 'acceptedValues' => '0, 1', 'sqlSegment' => 'visit_goal_converted', ); + + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => Piwik_Translate('General_EcommerceVisitStatus', '"&segment=visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart"'), + 'segment' => 'visitEcommerceStatus', + 'acceptedValues' => implode(", ", self::$visitEcommerceStatus), + 'sqlSegment' => 'visit_goal_buyer', + 'sqlFilter' => array('Piwik_API_API', 'getVisitEcommerceStatus'), + ); + + $segments[] = array( + 'type' => 'metric', + 'category' => 'Visit', + 'name' => 'General_DaysSinceLastEcommerceOrder', + 'segment' => 'daysSinceLastEcommerceOrder', + 'sqlSegment' => 'visitor_days_since_order', + ); + foreach ($segments as &$segment) { $segment['name'] = Piwik_Translate($segment['name']); @@ -217,6 +236,32 @@ class Piwik_API_API return $segments; } + static protected $visitEcommerceStatus = array( + Piwik_Tracker_GoalManager::TYPE_BUYER_NONE => 'none', + Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED => 'ordered', + Piwik_Tracker_GoalManager::TYPE_BUYER_OPEN_CART => 'abandonedCart', + Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART => 'orderedThenAbandonedCart', + ); + + static public function getVisitEcommerceStatusFromId($id) + { + if(!isset(self::$visitEcommerceStatus[$id])) + { + throw new Exception("Unexpected ECommerce status value "); + } + return self::$visitEcommerceStatus[$id]; + } + + static public function getVisitEcommerceStatus($status) + { + $id = array_search($status, self::$visitEcommerceStatus); + if($id === false) + { + throw new Exception("Invalid 'visitEcommerceStatus' segment value"); + } + return $id; + } + private function sortSegments($row1, $row2) { $columns = array('type', 'category', 'name', 'segment'); diff --git a/plugins/Actions/Actions.php b/plugins/Actions/Actions.php index c93aff5ec6e72d59f89a96b50b690fe10beeaae9..789a96731dbfe16a96fc1830632743768b9dca6d 100644 --- a/plugins/Actions/Actions.php +++ b/plugins/Actions/Actions.php @@ -320,12 +320,13 @@ class Piwik_Actions extends Piwik_Plugin count(distinct log_link_visit_action.idvisitor) as `". Piwik_Archive::INDEX_NB_UNIQ_VISITORS ."`, count(*) as `". Piwik_Archive::INDEX_PAGE_NB_HITS ."` FROM ".Piwik_Common::prefixTable('log_link_visit_action')." as log_link_visit_action - LEFT JOIN ".Piwik_Common::prefixTable('log_action')." as log_action ON (log_link_visit_action.%s = idaction) + LEFT JOIN ".Piwik_Common::prefixTable('log_action')." as log_action + ON (log_link_visit_action.%s = idaction) $sqlJoinVisitTable WHERE server_time >= ? AND server_time <= ? AND log_link_visit_action.idsite = ? - AND %s > 0 + AND %s IS NOT NULL $sqlSegmentWhere GROUP BY idaction ORDER BY `". Piwik_Archive::INDEX_PAGE_NB_HITS ."` DESC"; @@ -397,7 +398,7 @@ class Piwik_Actions extends Piwik_Plugin $queryString = str_replace("%s", $sprintfParameter, $queryString); $bind = array_merge(array( $archiveProcessing->getStartDatetimeUTC(), $archiveProcessing->getEndDatetimeUTC(), $archiveProcessing->idsite ), $bind); $resultSet = $archiveProcessing->db->query($queryString, $bind); - $modified = $this->updateActionsTableWithRowQuery($resultSet); + $modified = $this->updateActionsTableWithRowQuery($resultSet, $sprintfParameter); return $modified; } protected function archiveDayRecordInDatabase($archiveProcessing) @@ -522,11 +523,7 @@ class Piwik_Actions extends Piwik_Plugin if( empty($split) ) { - if($type == Piwik_Tracker_Action::TYPE_ACTION_NAME) { - $defaultName = self::$defaultActionNameWhenNotDefined; - } else { - $defaultName = self::$defaultActionUrlWhenNotDefined; - } + $defaultName = self::getUnknownActionName($type); return array( trim($defaultName) ); } @@ -546,15 +543,31 @@ class Piwik_Actions extends Piwik_Plugin return array_values( $split ); } + static protected function getUnknownActionName($type) + { + if($type == Piwik_Tracker_Action::TYPE_ACTION_NAME) { + return self::$defaultActionNameWhenNotDefined; + } + return self::$defaultActionUrlWhenNotDefined; + } + const CACHE_PARSED_INDEX_NAME = 0; const CACHE_PARSED_INDEX_TYPE = 1; static $cacheParsedAction = array(); - protected function updateActionsTableWithRowQuery($query) + protected function updateActionsTableWithRowQuery($query, $fieldQueried = false) { $rowsProcessed = 0; while( $row = $query->fetch() ) { + if(empty($row['idaction'])) + { + $row['type'] = ($fieldQueried == 'idaction_url' ? Piwik_Tracker_Action::TYPE_ACTION_URL : Piwik_Tracker_Action::TYPE_ACTION_NAME); + // This will be replaced with 'X not defined' later + $row['name'] = ''; + // Yes, this is kind of a hack, so we don't mix 'page url not defined' with 'page title not defined' etc. + $row['idaction'] = -$row['type']; + } // Only the first query will contain the name and type of actions, for performance reasons if(isset($row['name']) && isset($row['type'])) @@ -580,9 +593,6 @@ class Piwik_Actions extends Piwik_Plugin // - We select an entry page ID that was only seen yesterday, so wasn't selected in the first query // - We count time spent on a page, when this page was only seen yesterday continue; - var_dump($row); - debug_print_backtrace(); - throw new Exception("id action ". $row['idaction'] . " was not cached, but we expected it. Please report this issue in Piwik forums."); } $currentTable = self::$cacheParsedAction[$row['idaction']]; // Action processed as "to skip" for some reasons diff --git a/plugins/CustomVariables/CustomVariables.php b/plugins/CustomVariables/CustomVariables.php index 80c0e07501e22dc49729179a43776f3ae1c5e73e..e9da3d53906898187552afa6f1e7a14bfa34c1b6 100644 --- a/plugins/CustomVariables/CustomVariables.php +++ b/plugins/CustomVariables/CustomVariables.php @@ -171,8 +171,8 @@ class Piwik_CustomVariables extends Piwik_Plugin { while($row = $query->fetch() ) { - if(!isset($this->interestByCustomVariables[$row[$keyField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCustomVariables[$row[$keyField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); - if(!isset($this->interestByCustomVariablesAndValue[$row[$keyField]][$row[$valueField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCustomVariablesAndValue[$row[$keyField]][$row[$valueField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); + if(!isset($this->interestByCustomVariables[$row[$keyField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCustomVariables[$row[$keyField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + if(!isset($this->interestByCustomVariablesAndValue[$row[$keyField]][$row[$valueField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCustomVariablesAndValue[$row[$keyField]][$row[$valueField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); $archiveProcessing->updateGoalStats( $row, $this->interestByCustomVariables[$row[$keyField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); $archiveProcessing->updateGoalStats( $row, $this->interestByCustomVariablesAndValue[$row[$keyField]][$row[$valueField]][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); diff --git a/plugins/Goals/API.php b/plugins/Goals/API.php index 65d902813e856befecb0626ccb752e99753c9a14..2b3fadfbf3831f60357e653895cb3ebdf122f6e5 100644 --- a/plugins/Goals/API.php +++ b/plugins/Goals/API.php @@ -14,8 +14,19 @@ * Goals API lets you Manage existing goals, via "updateGoal" and "deleteGoal", create new Goals via "addGoal", * or list existing Goals for one or several websites via "getGoals" * - * It also lets you request overall Goal metrics via the method "get" and the additional helpers "getConversions", "getRevenue", etc. + * If you are tracking Ecommerce orders and products on your site, the functions "getItemsSku", "getItemsName" and "getItemsCategory" + * 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 + * metrics are returned: Total revenue, quantity, average price, average quantity, number of orders with this product. + * + * By default, these functions returns the 'Products purchased'. These functions also accept an optional parameter &abandonedCarts=1. + * If the parameter is set, it will instead return the metrics for products that were left in an abandoned cart therefore not purchased. + * + * The API also lets you request overall Goal metrics via the method "get": Conversions, Visits with at least one conversion, Conversion rate and Revenue. + * 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) + * or &idGoal=ecommerceOrder to get metrics about Ecommerce orders (number of orders, visits with an order, subtotal, tax, shipping, discount, revenue, items ordered) + * * See also the documentation about <a href='http://piwik.org/docs/tracking-goals-web-analytics/' target='_blank'>Tracking Goals</a> in Piwik. + * * @package Piwik_Goals */ class Piwik_Goals_API @@ -177,6 +188,53 @@ class Piwik_Goals_API Piwik_Common::regenerateCacheWebsiteAttributes($idSite); } + /** + * Returns a datatable of Items SKU/name or categories and their metrics + * If $abandonedCarts set to 1, will return items abandoned in carts. If set to 0, will return items ordered + */ + protected function getItems($recordName, $idSite, $period, $date, $abandonedCarts ) + { + Piwik::checkUserHasViewAccess( $idSite ); + if($abandonedCarts) + { + $recordName = Piwik_Goals::getItemRecordNameAbandonedCart($recordName); + } + $archive = Piwik_Archive::build($idSite, $period, $date ); + $dataTable = $archive->getDataTable($recordName); + $dataTable->filter('Sort', array(Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE)); + $dataTable->queueFilter('ReplaceColumnNames'); + + $ordersColumn = 'orders'; + if($abandonedCarts) + { + $ordersColumn = 'abandoned_carts'; + $dataTable->renameColumn(Piwik_Archive::INDEX_ECOMMERCE_ORDERS, $ordersColumn); + } + // Average price = sum product revenue / quantity + $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_price', 'price', $ordersColumn, Piwik_Tracker_GoalManager::REVENUE_PRECISION)); + + // Average quantity = sum product quantity / abandoned carts + $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('avg_quantity', 'quantity', $ordersColumn, $precision = 1)); + $dataTable->queueFilter('ColumnDelete', array('price')); + + return $dataTable; + } + + public function getItemsSku($idSite, $period, $date, $abandonedCarts = false ) + { + return $this->getItems('Goals_ItemsSku', $idSite, $period, $date, $abandonedCarts); + } + + public function getItemsName($idSite, $period, $date, $abandonedCarts = false ) + { + return $this->getItems('Goals_ItemsName', $idSite, $period, $date, $abandonedCarts); + } + + public function getItemsCategory($idSite, $period, $date, $abandonedCarts = false ) + { + return $this->getItems('Goals_ItemsCategory', $idSite, $period, $date, $abandonedCarts); + } + /** * Returns Goals data * @@ -193,14 +251,16 @@ class Piwik_Goals_API $archive = Piwik_Archive::build($idSite, $period, $date, $segment ); $columns = Piwik::getArrayFromApiParameter($columns); + // Mapping string idGoal to internal ID + $idGoal = ($idGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER) + ? Piwik_Tracker_GoalManager::IDGOAL_ORDER + : ($idGoal == Piwik_Archive::LABEL_ECOMMERCE_CART + ? Piwik_Tracker_GoalManager::IDGOAL_CART + : $idGoal); + if(empty($columns)) { - $columns = array( - 'nb_conversions', - 'nb_visits_converted', - 'conversion_rate', - 'revenue', - ); + $columns = Piwik_Goals::getGoalColumns($idGoal); } $columnsToSelect = array(); foreach($columns as &$columnName) @@ -225,21 +285,33 @@ class Piwik_Goals_API return $dataTable; } + /** + * @ignore + */ public function getConversions( $idSite, $period, $date, $segment = false, $idGoal = false ) { return $this->getNumeric( $idSite, $period, $date, $segment, Piwik_Goals::getRecordName('nb_conversions', $idGoal)); } + /** + * @ignore + */ public function getNbVisitsConverted( $idSite, $period, $date, $segment = false, $idGoal = false ) { return $this->getNumeric( $idSite, $period, $date, $segment, Piwik_Goals::getRecordName('nb_visits_converted', $idGoal)); } + /** + * @ignore + */ public function getConversionRate( $idSite, $period, $date, $segment = false, $idGoal = false ) { return $this->getNumeric( $idSite, $period, $date, $segment, Piwik_Goals::getRecordName('conversion_rate', $idGoal)); } + /** + * @ignore + */ public function getRevenue( $idSite, $period, $date, $segment = false, $idGoal = false ) { return $this->getNumeric( $idSite, $period, $date, $segment, Piwik_Goals::getRecordName('revenue', $idGoal)); diff --git a/plugins/Goals/Controller.php b/plugins/Goals/Controller.php index 96ed86b34dcddfaed7bc12142aaf74a39705bb58..728caec14f499c75d3f656a2c4b28d130b2e169a 100644 --- a/plugins/Goals/Controller.php +++ b/plugins/Goals/Controller.php @@ -83,9 +83,11 @@ class Piwik_Goals_Controller extends Piwik_Controller $view->topDimensions = $this->getTopDimensions($idGoal); // conversion rate for new and returning visitors - $conversionRateReturning = $this->getConversionRateReturningVisitors($this->idSite, Piwik_Common::getRequestVar('period'), Piwik_Common::getRequestVar('date'), $idGoal); + $segment = 'visitorType==returning'; + $conversionRateReturning = Piwik_Goals_API::getInstance()->getConversionRate($this->idSite, Piwik_Common::getRequestVar('period'), Piwik_Common::getRequestVar('date'), $segment, $idGoal); $view->conversion_rate_returning = $this->formatConversionRate($conversionRateReturning); - $conversionRateNew = $this->getConversionRateNewVisitors($this->idSite, Piwik_Common::getRequestVar('period'), Piwik_Common::getRequestVar('date'), $idGoal); + $segment = 'visitorType==new'; + $conversionRateNew = Piwik_Goals_API::getInstance()->getConversionRate($this->idSite, Piwik_Common::getRequestVar('period'), Piwik_Common::getRequestVar('date'), $segment, $idGoal); $view->conversion_rate_new = $this->formatConversionRate($conversionRateNew); return $view; } @@ -278,52 +280,4 @@ class Piwik_Goals_Controller extends Piwik_Controller 'urlSparklineRevenue' => $this->getUrlSparkline('getEvolutionGraph', array('columns' => array('revenue'), 'idGoal' => $idGoal)), ); } - - private function getConversionRateReturningVisitors( $idSite, $period, $date, $idGoal = false ) - { - // visits converted for returning for all goals = call Frequency API - if($idGoal === false) - { - $request = new Piwik_API_Request("method=VisitFrequency.getConvertedVisitsReturning&idSite=$idSite&period=$period&date=$date&format=original"); - $nbVisitsConvertedReturningVisitors = $request->process(); - } - // visits converted for returning = nb conversion for this goal - else - { - $nbVisitsConvertedReturningVisitors = Piwik_Goals_API::getInstance()->getConversions($idSite, $period, $date, $segment=false,$idGoal); - } - // all returning visits - $request = new Piwik_API_Request("method=VisitFrequency.getVisitsReturning&idSite=$idSite&period=$period&date=$date&format=original"); - $nbVisitsReturning = $request->process(); -// echo $nbVisitsConvertedReturningVisitors; -// echo "<br />". $nbVisitsReturning;exit; - - return Piwik::getPercentageSafe($nbVisitsConvertedReturningVisitors, $nbVisitsReturning, Piwik_Goals::ROUNDING_PRECISION); - } - - private function getConversionRateNewVisitors( $idSite, $period, $date, $idGoal = false ) - { - // new visits converted for all goals = nb visits converted - nb visits converted for returning - if($idGoal == false) - { - $request = new Piwik_API_Request("method=VisitsSummary.getVisitsConverted&idSite=$idSite&period=$period&date=$date&format=original"); - $convertedVisits = $request->process(); - $request = new Piwik_API_Request("method=VisitFrequency.getConvertedVisitsReturning&idSite=$idSite&period=$period&date=$date&format=original"); - $convertedReturningVisits = $request->process(); - $convertedNewVisits = $convertedVisits - $convertedReturningVisits; - } - // new visits converted for a given goal = nb conversion for this goal for new visits - else - { - $convertedNewVisits = Piwik_Goals_API::getInstance()->getConversions($idSite, $period, $date, $segment=false, $idGoal); - } - // all new visits = all visits - all returning visits - $request = new Piwik_API_Request("method=VisitFrequency.getVisitsReturning&idSite=$idSite&period=$period&date=$date&format=original"); - $nbVisitsReturning = $request->process(); - $request = new Piwik_API_Request("method=VisitsSummary.getVisits&idSite=$idSite&period=$period&date=$date&format=original"); - $nbVisits = $request->process(); - $newVisits = $nbVisits - $nbVisitsReturning; - return Piwik::getPercentageSafe($convertedNewVisits, $newVisits, Piwik_Goals::ROUNDING_PRECISION); - } - } diff --git a/plugins/Goals/Goals.php b/plugins/Goals/Goals.php index 4b1a15d3d3cb84487597146c1202656cfb289217..049117dc86bffe6313464e33a17bab773a1b720d 100644 --- a/plugins/Goals/Goals.php +++ b/plugins/Goals/Goals.php @@ -16,8 +16,6 @@ */ class Piwik_Goals extends Piwik_Plugin { - const ROUNDING_PRECISION = 2; - public function getInformation() { $info = array( @@ -206,22 +204,16 @@ class Piwik_Goals extends Piwik_Plugin /** * @param string $recordName 'nb_conversions' * @param int $idGoal idGoal to return the metrics for, or false to return overall - * @param int $visitorReturning 0 for new visitors, 1 for returning visitors, false for all - * @return unknown + * @return string Archive record name */ - static public function getRecordName($recordName, $idGoal = false, $visitorReturning = false) + static public function getRecordName($recordName, $idGoal = false) { - $idGoalStr = $returningStr = ''; - if(!empty($idGoal)) + $idGoalStr = ''; + if($idGoal !== false) { $idGoalStr = $idGoal . "_"; } - if($visitorReturning !== false) - { - $returningStr = 'visitor_returning_' . $visitorReturning . '_'; - } - - return 'Goal_' . $returningStr . $idGoalStr . $recordName; + return 'Goal_' . $idGoalStr . $recordName; } /** @@ -233,37 +225,86 @@ class Piwik_Goals extends Piwik_Plugin */ function archivePeriod($notification ) { + /** + * @var Piwik_ArchiveProcessing + */ $archiveProcessing = $notification->getNotificationObject(); if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; - $metricsToSum = array( 'nb_conversions', 'revenue'); - $goalIdsToSum = Piwik_Tracker_GoalManager::getGoalIds($archiveProcessing->idsite); + /* + * Archive Ecommerce Items + */ + if($this->shouldArchiveEcommerceItems($archiveProcessing)) + { + $dataTableToSum = $this->dimensions; + foreach($this->dimensions as $recordName) + { + $dataTableToSum[] = self::getItemRecordNameAbandonedCart($recordName); + } + $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum); + } + + /* + * Archive General Goal metrics + */ + $goalIdsToSum = Piwik_Tracker_GoalManager::getGoalIds($archiveProcessing->idsite); + if(Piwik::isEcommerceEnabled($archiveProcessing->idsite)) + { + $goalIdsToSum[] = Piwik_Tracker_GoalManager::IDGOAL_ORDER; + $goalIdsToSum[] = Piwik_Tracker_GoalManager::IDGOAL_CART; + // Overall goal metrics + $goalIdsToSum[] = false; + } $fieldsToSum = array(); - foreach($metricsToSum as $metricName) + foreach($goalIdsToSum as $goalId) { - foreach($goalIdsToSum as $goalId) + $metricsToSum = Piwik_Goals::getGoalColumns($goalId); + unset($metricsToSum[array_search('conversion_rate', $metricsToSum)]); + foreach($metricsToSum as $metricName) { $fieldsToSum[] = self::getRecordName($metricName, $goalId); - $fieldsToSum[] = self::getRecordName($metricName, $goalId, 0); - $fieldsToSum[] = self::getRecordName($metricName, $goalId, 1); } - $fieldsToSum[] = self::getRecordName($metricName); } $records = $archiveProcessing->archiveNumericValuesSum($fieldsToSum); // also recording conversion_rate for each goal foreach($goalIdsToSum as $goalId) { - $nb_conversions = $records[self::getRecordName('nb_conversions', $goalId)]; + $nb_conversions = $records[self::getRecordName('nb_visits_converted', $goalId)]; $conversion_rate = $this->getConversionRate($nb_conversions, $archiveProcessing); $archiveProcessing->insertNumericRecord(self::getRecordName('conversion_rate', $goalId), $conversion_rate); + } + } + + static public function getGoalColumns($idGoal) + { + $columns = array( + 'nb_conversions', + 'nb_visits_converted', + 'conversion_rate', + 'revenue', + ); + if($idGoal === false) + { + return $columns; } - - // global conversion rate - $nb_conversions = $records[self::getRecordName('nb_conversions')]; - $conversion_rate = $this->getConversionRate($nb_conversions, $archiveProcessing); - $archiveProcessing->insertNumericRecord(self::getRecordName('conversion_rate'), $conversion_rate); + // Orders + if($idGoal === Piwik_Tracker_GoalManager::IDGOAL_ORDER) + { + $columns = array_merge($columns, array( + 'revenue_subtotal', + 'revenue_tax', + 'revenue_shipping', + 'revenue_discount', + )); + } + // Abandoned carts & orders + if($idGoal <= Piwik_Tracker_GoalManager::IDGOAL_ORDER) + { + $columns[] = 'items'; + } + return $columns; } /** @@ -283,25 +324,34 @@ class Piwik_Goals extends Piwik_Plugin if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; - // by processing visitor_returning segment, we can also simply sum and get stats for all goals. - $query = $archiveProcessing->queryConversionsByDimension('visitor_returning'); + $this->archiveGeneralGoalMetrics($archiveProcessing); + $this->archiveEcommerceItems($archiveProcessing); + } + + /** + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + */ + function archiveGeneralGoalMetrics($archiveProcessing) + { + $query = $archiveProcessing->queryConversionsByDimension(''); - if($query === false) return; + if($query === false) { return; } - $nb_conversions = $revenue = $nb_visits_converted = 0; - $goals = $goalsByVisitorReturning = array(); + $goals = array(); + // Get a standard empty goal row + $overall = $archiveProcessing->getNewGoalRow( $idGoal = 1); while($row = $query->fetch() ) { - $goalsByVisitorReturning[$row['idgoal']][$row['label']] = $archiveProcessing->getGoalRowFromQueryRow($row); - - if(!isset($goals[$row['idgoal']])) $goals[$row['idgoal']] = $archiveProcessing->getNewGoalRow(); + if(!isset($goals[$row['idgoal']])) $goals[$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); $archiveProcessing->updateGoalStats($row, $goals[$row['idgoal']]); - - $revenue += $row[Piwik_Archive::INDEX_GOAL_REVENUE]; - $nb_conversions += $row[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; - $nb_visits_converted += $row[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED]; + + // We don't want to sum Abandoned cart metrics in the overall revenue/conversions/converted visits + // since it is a "negative conversion" + if($row['idgoal'] != Piwik_Tracker_GoalManager::IDGOAL_CART) + { + $archiveProcessing->updateGoalStats($row, $overall); + } } - // Stats by goal, for all visitors foreach($goals as $idgoal => $values) { @@ -311,32 +361,19 @@ class Piwik_Goals extends Piwik_Plugin $recordName = self::getRecordName($metricName, $idgoal); $archiveProcessing->insertNumericRecord($recordName, $value); } - $conversion_rate = $this->getConversionRate($values[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS], $archiveProcessing); + + $conversion_rate = $this->getConversionRate($values[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED], $archiveProcessing); $recordName = self::getRecordName('conversion_rate', $idgoal); $archiveProcessing->insertNumericRecord($recordName, $conversion_rate); } - // Stats by goal, for visitor returning / non returning - foreach($goalsByVisitorReturning as $idgoal => $values) - { - foreach($values as $visitor_returning => $goalValues) - { - foreach($goalValues as $metricId => $value) - { - $metricName = Piwik_Archive::$mappingFromIdToNameGoal[$metricId]; - $recordName = self::getRecordName($metricName, $idgoal, $visitor_returning); - $archiveProcessing->insertNumericRecord($recordName, $value); -// echo $record . "<br />"; - } - } - } - + // Stats for all goals $totalAllGoals = array( self::getRecordName('conversion_rate') => $this->getConversionRate($archiveProcessing->getNumberOfVisitsConverted(), $archiveProcessing), - self::getRecordName('nb_conversions') => $nb_conversions, - self::getRecordName('nb_visits_converted') => $nb_visits_converted, - self::getRecordName('revenue') => $revenue, + self::getRecordName('nb_conversions') => $overall[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS], + self::getRecordName('nb_visits_converted') => $archiveProcessing->getNumberOfVisitsConverted(), + self::getRecordName('revenue') => $overall[Piwik_Archive::INDEX_GOAL_REVENUE], ); foreach($totalAllGoals as $recordName => $value) { @@ -344,10 +381,99 @@ class Piwik_Goals extends Piwik_Plugin } } + protected $dimensions = array( + 'idaction_sku' => 'Goals_ItemsSku', + 'idaction_name' => 'Goals_ItemsName', + 'idaction_category' => 'Goals_ItemsCategory' + ); + + protected function shouldArchiveEcommerceItems($archiveProcessing) + { + if(!Piwik::isEcommerceEnabled($archiveProcessing->idsite) + // 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 + // (if this is implented, we should have shouldProcessReportsForPlugin() support partial archiving based on which metric is requested) + || !$archiveProcessing->getSegment()->isEmpty()) + { + return false; + } + return true; + } + + /** + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + */ + function archiveEcommerceItems($archiveProcessing) + { + if(!$this->shouldArchiveEcommerceItems($archiveProcessing)) + { + return false; + } + $dimensionsNotSet = array( + 'idaction_sku' => Piwik_Translate('General_NotDefined', Piwik_Translate('Goals_ProductSKU')), // Note: this should never happen + 'idaction_name' => Piwik_Translate('General_NotDefined', Piwik_Translate('Goals_ProductName')), + 'idaction_category' => Piwik_Translate('General_NotDefined', Piwik_Translate('Goals_ProductCategory')) + ); + $items = array(); + foreach($this->dimensions as $dimension => $recordName) + { + $query = $archiveProcessing->queryEcommerceItems($dimension); + if($query == false) { return; } + + while($row = $query->fetch()) + { + $label = $row['label']; + $ecommerceType = $row['type']; + + if(empty($label)) + { + $label = $dimensionsNotSet[$dimension]; + } + // 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['type']); + if($row[Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE] == round($row[Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE])) + { + $row[Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE] = round($row[Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE]); + } + $items[$dimension][$ecommerceType][$label] = $row; + } + } + + 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; + if($ecommerceType == Piwik_Tracker_GoalManager::IDGOAL_CART) + { + $recordNameInsert = self::getItemRecordNameAbandonedCart($recordName); + } + $table = $archiveProcessing->getDataTableFromArray($items[$dimension][$ecommerceType]); + $archiveProcessing->insertBlobRecord($recordNameInsert, $table->getSerialized()); + } + } + } + + static public function getItemRecordNameAbandonedCart($recordName) + { + return $recordName . '_Cart'; + } + function getConversionRate($count, $archiveProcessing) { $visits = $archiveProcessing->getNumberOfVisits(); - return round(100 * $count / $visits, self::ROUNDING_PRECISION); + return round(100 * $count / $visits, Piwik_Tracker_GoalManager::REVENUE_PRECISION); } } diff --git a/plugins/Live/API.php b/plugins/Live/API.php index 3c74dd3da1fda3cbc0f4c458dba0734c496caf69..3460c8387183ef1b44fabc7d9fad2b2f09fc1953 100644 --- a/plugins/Live/API.php +++ b/plugins/Live/API.php @@ -202,10 +202,39 @@ class Piwik_Live_API goal.idgoal = log_conversion.idgoal) AND goal.deleted = 0 WHERE log_conversion.idvisit = ? + AND log_conversion.idgoal > 0 "; $goalDetails = Piwik_FetchAll($sql, array($idvisit)); - $actions = array_merge($actionDetails, $goalDetails); + $sql = "SELECT + case idgoal when ".Piwik_Tracker_GoalManager::IDGOAL_CART." then '".Piwik_Archive::LABEL_ECOMMERCE_CART."' else '".Piwik_Archive::LABEL_ECOMMERCE_ORDER."' end as type, + idorder as orderId, + revenue as revenue, + revenue_subtotal as revenueSubTotal, + revenue_tax as revenueTax, + revenue_shipping as revenueShipping, + revenue_discount as revenueDiscount, + items as items, + + log_conversion.server_time as serverTimePretty + FROM ".Piwik_Common::prefixTable('log_conversion')." AS log_conversion + WHERE idvisit = ? + AND idgoal <= ".Piwik_Tracker_GoalManager::IDGOAL_ORDER; + $ecommerceDetails = Piwik_FetchAll($sql, array($idvisit)); + + foreach($ecommerceDetails as &$ecommerceDetail) + { + if($ecommerceDetail['type'] == Piwik_Archive::LABEL_ECOMMERCE_CART) + { + unset($ecommerceDetail['orderId']); + unset($ecommerceDetail['revenueSubTotal']); + unset($ecommerceDetail['revenueTax']); + unset($ecommerceDetail['revenueShipping']); + unset($ecommerceDetail['revenueDiscount']); + } + } + + $actions = array_merge($actionDetails, $goalDetails, $ecommerceDetails); usort($actions, array($this, 'sortByServerTime')); @@ -216,6 +245,8 @@ class Piwik_Live_API switch($details['type']) { case 'goal': + case Piwik_Archive::LABEL_ECOMMERCE_ORDER: + case Piwik_Archive::LABEL_ECOMMERCE_CART: break; case Piwik_Tracker_Action_Interface::TYPE_DOWNLOAD: $details['type'] = 'download'; @@ -232,6 +263,39 @@ class Piwik_Live_API } $visitorDetailsArray['goalConversions'] = count($goalDetails); + // Enrich ecommerce carts/orders with the list of products + usort($ecommerceDetails, array($this, 'sortByServerTime')); + foreach($ecommerceDetails as $key => $ecommerceConversion) + { + $sql = "SELECT + log_action_sku.name as itemSKU, + log_action_name.name as itemName, + log_action_category.name as itemCategory, + price as price, + quantity as quantity + FROM ".Piwik_Common::prefixTable('log_conversion_item')." + INNER JOIN " .Piwik_Common::prefixTable('log_action')." AS log_action_sku + ON idaction_sku = log_action_sku.idaction + LEFT JOIN " .Piwik_Common::prefixTable('log_action')." AS log_action_name + ON idaction_name = log_action_name.idaction + LEFT JOIN " .Piwik_Common::prefixTable('log_action')." AS log_action_category + ON idaction_category = log_action_category.idaction + WHERE idvisit = ? + AND idorder = ? + AND deleted = 0 + "; + $bind = array($idvisit, isset($ecommerceConversion['orderId']) ? $ecommerceConversion['orderId'] : Piwik_Tracker_GoalManager::ITEM_IDORDER_ABANDONED_CART); + + $itemsDetails = Piwik_FetchAll($sql, $bind); + + // unreference the array or items added to the reference will show up in the 'actionDetails' + $value = $ecommerceDetails[$key]; + unset($ecommerceDetails[$key]); + $value['itemDetails'] = $itemsDetails; + $ecommerceDetails[$key] = $value; + } + + $visitorDetailsArray['ecommerce'] = $ecommerceDetails; $table->addRowFromArray( array(Piwik_DataTable_Row::COLUMNS => $visitorDetailsArray)); } return $table; @@ -305,7 +369,14 @@ class Piwik_Live_API } else { - $processedDate = Piwik_Date::factory($date)->subDay(1); + $processedDate = Piwik_Date::factory($date); + + if($date == 'today' + || $date == 'now' + || $processedDate == Piwik_Date::factory('today')) + { + $processedDate = $processedDate->subDay(1); + } $processedPeriod = Piwik_Period::factory($period, $processedDate); } $dateStart = $processedPeriod->getDateStart()->setTimezone($currentTimezone); diff --git a/plugins/Live/Visitor.php b/plugins/Live/Visitor.php index 1d0315920196c3bb2541378e5a36aa2efd5d84bc..77b97c0c811a0351e69ee93ac3f04f37a2e48dc7 100644 --- a/plugins/Live/Visitor.php +++ b/plugins/Live/Visitor.php @@ -44,6 +44,7 @@ class Piwik_Live_Visitor 'visitorId' => $this->getVisitorId(), 'visitorType' => $this->isVisitorReturning() ? 'returning' : 'new', 'visitConverted' => $this->isVisitorGoalConverted(), + 'visitEcommerceStatus' => $this->getVisitEcommerceStatus(), 'actions' => $this->getNumberOfActions(), // => false are placeholders to be filled in API later @@ -65,6 +66,7 @@ class Piwik_Live_Visitor 'visitCount' => $this->getVisitCount(), 'daysSinceLastVisit' => $this->getDaysSinceLastVisit(), 'daysSinceFirstVisit' => $this->getDaysSinceFirstVisit(), + 'daysSinceLastEcommerceOrder' => $this->getDaysSinceLastEcommerceOrder(), 'country' => $this->getCountryName(), 'countryFlag' => $this->getCountryFlag(), 'continent' => $this->getContinent(), @@ -117,6 +119,10 @@ class Piwik_Live_Visitor return $this->details['visitor_days_since_last']; } + function getDaysSinceLastEcommerceOrder() + { + return $this->details['visitor_days_since_order']; + } function getDaysSinceFirstVisit() { return $this->details['visitor_days_since_first']; @@ -379,6 +385,11 @@ class Piwik_Live_Visitor return date('Y-m-d H:i:s', strtotime($this->details['visit_last_action_time'])); } + function getVisitEcommerceStatus() + { + return Piwik_API_API::getVisitEcommerceStatusFromId($this->details['visit_goal_buyer']); + } + function isVisitorGoalConverted() { return $this->details['visit_goal_converted']; diff --git a/plugins/PDFReports/API.php b/plugins/PDFReports/API.php index 38d8a8a20b7b90fb073d339cf77d7c2d609203c0..22b1bde7a777086e34d2efdcd015791ef468c38c 100644 --- a/plugins/PDFReports/API.php +++ b/plugins/PDFReports/API.php @@ -355,7 +355,6 @@ class Piwik_PDFReports_API { $reports = $this->getReports($idSite, $period = false, $idReport); $report = reset($reports); - if($report['period'] == 'never') { $report['period'] = 'day'; diff --git a/plugins/Referers/Referers.php b/plugins/Referers/Referers.php index 092e774a2b7d91b7a98776eab0091a8b94ab1e12..4d5fa40697977999ed058e52bf8d514d8b1ac288 100644 --- a/plugins/Referers/Referers.php +++ b/plugins/Referers/Referers.php @@ -429,25 +429,25 @@ class Piwik_Referers extends Piwik_Plugin switch($row['referer_type']) { case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE: - if(!isset($this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); - if(!isset($this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); + if(!isset($this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + if(!isset($this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); $archiveProcessing->updateGoalStats( $row, $this->interestBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); $archiveProcessing->updateGoalStats( $row, $this->interestByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); break; case Piwik_Common::REFERER_TYPE_WEBSITE: - if(!isset($this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); + if(!isset($this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); $archiveProcessing->updateGoalStats( $row, $this->interestByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); break; case Piwik_Common::REFERER_TYPE_CAMPAIGN: if(!empty($row['referer_keyword'])) { - if(!isset($this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); + if(!isset($this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); $archiveProcessing->updateGoalStats( $row, $this->interestByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); } - if(!isset($this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); + if(!isset($this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); $archiveProcessing->updateGoalStats( $row, $this->interestByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); break; @@ -457,7 +457,7 @@ class Piwik_Referers extends Piwik_Plugin break; } } - if(!isset($this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] )) $this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); + if(!isset($this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] )) $this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); $archiveProcessing->updateGoalStats($row, $this->interestByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); } diff --git a/plugins/UserCountry/UserCountry.php b/plugins/UserCountry/UserCountry.php index fc72df6e6da02d3d8292c1cab313c8e0d19d5727..8fcb565da6ecd8aa07be66ad3bb23565767e59b8 100644 --- a/plugins/UserCountry/UserCountry.php +++ b/plugins/UserCountry/UserCountry.php @@ -115,6 +115,9 @@ class Piwik_UserCountry extends Piwik_Plugin function archivePeriod( $notification ) { + /** + * @param Piwik_ArchiveProcessing_Period $archiveProcessing + */ $archiveProcessing = $notification->getNotificationObject(); if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; @@ -131,6 +134,9 @@ class Piwik_UserCountry extends Piwik_Plugin function archiveDay($notification) { + /** + * @var Piwik_ArchiveProcessing + */ $archiveProcessing = $notification->getNotificationObject(); if(!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return; @@ -140,6 +146,9 @@ class Piwik_UserCountry extends Piwik_Plugin $this->archiveDayRecordInDatabase($archiveProcessing); } + /** + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + */ protected function archiveDayAggregateVisits($archiveProcessing) { $labelSQL = "location_country"; @@ -149,6 +158,9 @@ class Piwik_UserCountry extends Piwik_Plugin $this->interestByContinent = $archiveProcessing->getArrayInterestForLabel($labelSQL); } + /** + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + */ protected function archiveDayAggregateGoals($archiveProcessing) { $query = $archiveProcessing->queryConversionsByDimension(array("location_continent","location_country")); @@ -157,8 +169,8 @@ class Piwik_UserCountry extends Piwik_Plugin while($row = $query->fetch() ) { - if(!isset($this->interestByCountry[$row['location_country']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCountry[$row['location_country']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); - if(!isset($this->interestByContinent[$row['location_continent']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByContinent[$row['location_continent']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow(); + if(!isset($this->interestByCountry[$row['location_country']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByCountry[$row['location_country']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); + if(!isset($this->interestByContinent[$row['location_continent']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->interestByContinent[$row['location_continent']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->getNewGoalRow($row['idgoal']); $archiveProcessing->updateGoalStats($row, $this->interestByCountry[$row['location_country']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); $archiveProcessing->updateGoalStats($row, $this->interestByContinent[$row['location_continent']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]); } @@ -166,6 +178,9 @@ class Piwik_UserCountry extends Piwik_Plugin $archiveProcessing->enrichConversionsByLabelArray($this->interestByContinent); } + /** + * @param Piwik_ArchiveProcessing_Day $archiveProcessing + */ protected function archiveDayRecordInDatabase($archiveProcessing) { $tableCountry = $archiveProcessing->getDataTableFromArray($this->interestByCountry); diff --git a/plugins/VisitFrequency/API.php b/plugins/VisitFrequency/API.php index 8cb246bd3229818a6758fe81de157055a6c4e9c2..074377974d60f6a766bf5581d77b37974fe376ac 100644 --- a/plugins/VisitFrequency/API.php +++ b/plugins/VisitFrequency/API.php @@ -97,26 +97,41 @@ class Piwik_VisitFrequency_API return $dataTable; } + /** + * @ignore + */ public function getVisitsReturning( $idSite, $period, $date ) { return $this->getNumeric( $idSite, $period, $date, 'nb_visits_returning'); } + /** + * @ignore + */ public function getActionsReturning( $idSite, $period, $date ) { return $this->getNumeric( $idSite, $period, $date, 'nb_actions_returning'); } + /** + * @ignore + */ public function getSumVisitsLengthReturning( $idSite, $period, $date ) { return $this->getNumeric( $idSite, $period, $date, 'sum_visit_length_returning'); } + /** + * @ignore + */ public function getBounceCountReturning( $idSite, $period, $date ) { return $this->getNumeric( $idSite, $period, $date, 'bounce_count_returning'); } + /** + * @ignore + */ public function getConvertedVisitsReturning( $idSite, $period, $date ) { return $this->getNumeric( $idSite, $period, $date, 'nb_visits_converted_returning'); diff --git a/tests/integration/Integration.php b/tests/integration/Integration.php index 2edabc6d8cc39a1c26c4f58d06dccdc9bd418701..954a5d7097e435223e9942c7106f46c308bc5b26 100644 --- a/tests/integration/Integration.php +++ b/tests/integration/Integration.php @@ -277,6 +277,7 @@ abstract class Test_Integration extends Test_Database $parametersToSet['format'] = $format; $parametersToSet['hideIdSubDatable'] = 1; $parametersToSet['serialize'] = 1; + $exampleUrl = $apiMetadata->getExampleUrl($class, $methodName, $parametersToSet); if($exampleUrl === false) { @@ -320,10 +321,11 @@ abstract class Test_Integration extends Test_Database * @param $language 2 letter language code to request data in * @param $segment Custom Segment to query the data for * @param $visitorId Only used for Live! API testing + * @param $abandonedCarts Only used in Goals API testing * * @return bool Passed or failed */ - function callGetApiCompareOutput($testName, $formats = 'xml', $idSite = false, $dateTime = false, $periods = false, $setDateLastN = false, $language = false, $segment = false, $visitorId = false) + function callGetApiCompareOutput($testName, $formats = 'xml', $idSite = false, $dateTime = false, $periods = false, $setDateLastN = false, $language = false, $segment = false, $visitorId = false, $abandonedCarts = false, $idGoal = false) { $pass = true; @@ -367,6 +369,7 @@ abstract class Test_Integration extends Test_Database 'showTimer' => 0, 'language' => $language ? $language : 'en', + 'abandonedCarts' => $abandonedCarts ? 1 : 0, ); if(!empty($visitorId )) { @@ -376,6 +379,10 @@ abstract class Test_Integration extends Test_Database { $parametersToSet['segment'] = $segment; } + if($idGoal !== false) + { + $parametersToSet['idGoal'] = $idGoal; + } // Give it enough time for the current API test to finish (call all get* APIs) Zend_Registry::get('config')->General->time_before_today_archive_considered_outdated = 10; @@ -383,8 +390,8 @@ abstract class Test_Integration extends Test_Database foreach($requestUrls as $apiId => $requestUrl) { - $isLiveMustDeleteDates = strpos($requestUrl, 'Live.getLastVisits') !== false; // echo "$requestUrl <br>"; + $isLiveMustDeleteDates = strpos($requestUrl, 'Live.getLastVisits') !== false; $request = new Piwik_API_Request($requestUrl); // $TEST_NAME - $API_METHOD diff --git a/tests/integration/Main.test.php b/tests/integration/Main.test.php index 322b732d5e40865714559b0ccf2fecdc744fbb7b..25fda0552277252b1c29be64dec3f6bec4aecfbc 100644 --- a/tests/integration/Main.test.php +++ b/tests/integration/Main.test.php @@ -31,6 +31,116 @@ class Test_Piwik_Integration_Main extends Test_Integration return PIWIK_INCLUDE_PATH . '/tests/integration'; } + function test_ecommerceOrderWithItems() + { + $this->setApiNotToCall(array()); + $dateTime = '2011-04-05 00:11:42'; + $idSite = $this->createWebsite($dateTime); + + $idGoal = Piwik_Goals_API::getInstance()->addGoal($idSite, 'triggered js ONCE', 'title', 'incredible', 'contains', $caseSensitive=false, $revenue=10, $allowMultipleConversions = true); + + $t = $this->getTracker($idSite, $dateTime, $defaultInit = true); + // Record 1st page view + $t->setUrl( 'http://example.org/index.htm' ); + $this->checkResponse($t->doTrackPageView( 'incredible title!')); + + $t->setForceVisitDateTime(Piwik_Date::factory($dateTime)->addHour(0.3)->getDatetime()); + + //Add to cart + $t->addEcommerceItem($sku = 'SKU VERY nice indeed', $name = 'PRODUCT name' , $category = 'Electronics & Cameras', $price = 500, $quantity = 1); + $t->addEcommerceItem($sku = 'SKU VERY nice indeed', $name = 'PRODUCT name' , $category = 'Electronics & Cameras', $price = 500, $quantity = 2); + $this->checkResponse($t->doTrackEcommerceCartUpdate($grandTotal = 1000)); + + $t->setForceVisitDateTime(Piwik_Date::factory($dateTime)->addHour(0.4)->getDatetime()); + //Order + $t->addEcommerceItem($sku = 'SKU VERY nice indeed', $name = 'PRODUCT name' , $category = 'Electronics & Cameras', $price = 500, $quantity = 2); + $this->checkResponse($t->doTrackEcommerceOrder($orderId = '937nsjusu 3894', $grandTotal = 1111.11, $subTotal = 1000, $tax = 111, $shipping = 0.11, $discount = 666)); + + $t->setForceVisitDateTime(Piwik_Date::factory($dateTime)->addHour(0.5)->getDatetime()); + //Another Order + $t->addEcommerceItem($sku = 'SKU2', $name = 'Canon SLR' , $category = 'Electronics & Cameras', $price = 1500, $quantity = 1); + $this->checkResponse($t->doTrackEcommerceOrder($orderId = '1037nsjusu4s3894', $grandTotal = 2000, $subTotal = 1500, $tax = 400, $shipping = 100, $discount = 0)); + + // Refresh the page with the receipt for the second order, should be ignored + $t->setForceVisitDateTime(Piwik_Date::factory($dateTime)->addHour(0.55)->getDatetime()); + + // Recording the same ecommerce order, this time with some crazy amount and quantity + // we test that both the order, and the products, are not updated on subsequent "Receipt" views + $t->addEcommerceItem($sku = 'SKU2', $name = 'Canon SLR' , $category = 'Electronics & Cameras', $price = 15000000000, $quantity = 10000); + $this->checkResponse($t->doTrackEcommerceOrder($orderId = '1037nsjusu4s3894', $grandTotal = 20000000, $subTotal = 1500, $tax = 400, $shipping = 100, $discount = 0)); + + // Leave with an opened cart + $t->setForceVisitDateTime(Piwik_Date::factory($dateTime)->addHour(0.6)->getDatetime()); + // No category + $t->addEcommerceItem($sku = 'SKU IN ABANDONED CART ONE', $name = 'PRODUCT ONE LEFT in cart' , $category = '', $price = 500.11111112, $quantity = 2); + $this->checkResponse($t->doTrackEcommerceCartUpdate($grandTotal = 1000)); + + // Record the same visit leaving twice an abandoned cart + foreach(array(0, 5, 24) as $offsetHour) + { + $t->setForceVisitDateTime(Piwik_Date::factory($dateTime)->addHour($offsetHour + 0.65)->getDatetime()); + // Also recording an order the day after + if($offsetHour >= 24) + { + $t->addEcommerceItem($sku = 'SKU2', $name = 'Canon SLR' , $category = 'Electronics & Cameras', $price = 1500, $quantity = 1); + $this->checkResponse($t->doTrackEcommerceOrder($orderId = '1037nsjusu4s3894', $grandTotal = 20000000, $subTotal = 1500, $tax = 400, $shipping = 100, $discount = 0)); + } + $t->addEcommerceItem($sku = 'SKU IN ABANDONED CART ONE', $name = 'PRODUCT ONE LEFT in cart' , $category = '', $price = 500.11111112, $quantity = 1); + $t->addEcommerceItem($sku = 'SKU IN ABANDONED CART TWO', $name = 'PRODUCT TWO LEFT in cart' , $category = 'Category TWO LEFT in cart', $price = 1000, $quantity = 2); + $this->checkResponse($t->doTrackEcommerceCartUpdate($grandTotal = 2500.11111112)); + } + + // One more Ecommerce order to check weekly archiving works fine on orders + $t->setForceVisitDateTime(Piwik_Date::factory($dateTime)->addHour( 30.65 )->getDatetime()); + $t->addEcommerceItem($sku = 'TRIPOD SKU', $name = 'TRIPOD - bought day after' , $category = 'Tools', $price = 100, $quantity = 2); + $this->checkResponse($t->doTrackEcommerceOrder($orderId = '666', $grandTotal = 240, $subTotal = 200, $tax = 20, $shipping = 20, $discount = 20)); + + // One more Ecommerce order, without any product in it, because we still track orders without products + $t->setForceVisitDateTime(Piwik_Date::factory($dateTime)->addHour( 30.75 )->getDatetime()); + $this->checkResponse($t->doTrackEcommerceOrder($orderId = '777', $grandTotal = 10000)); + + //------------------------------------- End tracking + + // From Piwik 1.5, we hide Goals.getConversions and other get* methods via @ignore, but we ensure that they still work + // This hack allows the API proxy to let us generate example URLs for the ignored functions + Piwik_API_Proxy::getInstance()->hideIgnoredFunctions = false; + + $this->setApiToCall( array('Live.getLastVisitsDetails', 'UserCountry', 'API.getProcessedReport', 'Goals.get', 'Goals.getConversions', 'Goals.getItemsSku', 'Goals.getItemsName', 'Goals.getItemsCategory' ) ); + $this->callGetApiCompareOutput(__FUNCTION__, 'xml', $idSite, $dateTime, $periods = array('day')); + + $this->setApiToCall( array('Goals.get', 'Goals.getItemsSku', 'Goals.getItemsName', 'Goals.getItemsCategory' ) ); + $this->callGetApiCompareOutput(__FUNCTION__, 'xml', $idSite, $dateTime, $periods = array('week')); + + $abandonedCarts = 1; + $this->setApiToCall( array('Goals.getItemsSku', 'Goals.getItemsName', 'Goals.getItemsCategory') ); + $this->callGetApiCompareOutput(__FUNCTION__ . '_AbandonedCarts', 'xml', $idSite, $dateTime, $periods = array('day', 'week'), $setDateLastN = false, $language = false, $segment = false, $visitorId = false, $abandonedCarts); + + // Test Goals.get with idGoal=ecommerceOrder and ecommerceAbandonedCart + $this->setApiToCall( array('Goals.get') ); + $idGoal = Piwik_Archive::LABEL_ECOMMERCE_CART; + $this->callGetApiCompareOutput(__FUNCTION__ . '_GoalAbandonedCart', 'xml', $idSite, $dateTime, $periods = array('day', 'week'), $setDateLastN = false, $language = false, $segment = false, $visitorId = false, $abandonedCarts = false, $idGoal); + + $idGoal = Piwik_Archive::LABEL_ECOMMERCE_ORDER; + $this->callGetApiCompareOutput(__FUNCTION__ . '_GoalOrder', 'xml', $idSite, $dateTime, $periods = array('day', 'week'), $setDateLastN = false, $language = false, $segment = false, $visitorId = false, $abandonedCarts = false, $idGoal); + $idGoal = 1; + $this->callGetApiCompareOutput(__FUNCTION__ . '_GoalMatchTitle', 'xml', $idSite, $dateTime, $periods = array('day', 'week'), $setDateLastN = false, $language = false, $segment = false, $visitorId = false, $abandonedCarts = false, $idGoal); + $idGoal = ''; + $this->callGetApiCompareOutput(__FUNCTION__ . '_GoalOverall', 'xml', $idSite, $dateTime, $periods = array('day', 'week'), $setDateLastN = false, $language = false, $segment = false, $visitorId = false, $abandonedCarts = false, $idGoal); + + $this->setApiToCall( array('VisitsSummary.get') ); + $segment = 'visitEcommerceStatus==none'; + $this->callGetApiCompareOutput(__FUNCTION__ . '_SegmentNoEcommerce', 'xml', $idSite, $dateTime, $periods = array('day'), $setDateLastN = false, $language = false, $segment); + $segment = 'visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart'; + $this->callGetApiCompareOutput(__FUNCTION__ . '_SegmentOrderedSomething', 'xml', $idSite, $dateTime, $periods = array('day'), $setDateLastN = false, $language = false, $segment); + $segment = 'visitEcommerceStatus==abandonedCart,visitEcommerceStatus==orderedThenAbandonedCart'; + $this->callGetApiCompareOutput(__FUNCTION__ . '_SegmentAbandonedCart', 'xml', $idSite, $dateTime, $periods = array('day'), $setDateLastN = false, $language = false, $segment); + + // test Live! output is OK also for the visit that just bought something (other visits leave an abandoned cart) + $this->setApiToCall(array('Live.getLastVisitsDetails')); + $this->callGetApiCompareOutput(__FUNCTION__ . '_LiveEcommerceStatusOrdered', 'xml', $idSite, Piwik_Date::factory($dateTime)->addHour( 30.65 )->getDatetime(), $periods = array('day')); +// exit; + } + function test_trackGoals_allowMultipleConversionsPerVisit() { $this->setApiToCall(array( @@ -312,8 +422,6 @@ class Test_Piwik_Integration_Main extends Test_Integration 'Actions.getPageUrls', 'Actions.getPageTitles', 'Actions.getOutlinks')); - ob_start(); - // - // First visitor on Idsite 1: two page views $datetimeSpanOverTwoDays = '2010-01-03 23:55:00'; @@ -322,6 +430,7 @@ class Test_Piwik_Integration_Main extends Test_Integration $visitorA->setUrl('http://example.org/homepage'); $this->checkResponse($visitorA->doTrackPageView('first page view')); $visitorA->setForceVisitDateTime(Piwik_Date::factory($datetimeSpanOverTwoDays)->addHour(0.1)->getDatetime()); + // Testing with empty URL and empty page title $visitorA->setUrl(' '); $this->checkResponse($visitorA->doTrackPageView(' ')); @@ -380,7 +489,6 @@ class Test_Piwik_Integration_Main extends Test_Integration // We also test a single period to check that this use case (Reports per idSite in the response) works $this->setApiToCall(array('VisitsSummary.get', 'Goals.get')); $this->callGetApiCompareOutput(__FUNCTION__ . '_NotLastNPeriods', 'xml', $idSite = 'all', $dateTime, array('day', 'month'), $setDateLastN = false); - } private function doTest_twoVisitsWithCustomVariables($dateTime, $width=1111, $height=222) @@ -569,6 +677,10 @@ class Test_Piwik_Integration_Main extends Test_Integration $seenVisitorId = true; $value = '34c31e04394bdc63'; } + if($segment['segment'] == 'visitEcommerceStatus') + { + $value = 'none'; + } $segmentExpression[] = $segment['segment'] .'!='.$value; } // just checking that this segment was tested (as it has the only visible to admin flag) diff --git a/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsCategory_day.xml b/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsCategory_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsName_day.xml b/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsName_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsSku_day.xml b/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsSku_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsCategory_day.xml b/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsCategory_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsName_day.xml b/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsName_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsSku_day.xml b/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsSku_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_PiwikTracker_trackForceUsingVisitId_insteadOfHeuristics_alsoTestsCampaignTracking__Referers.getCampaigns_day.xml b/tests/integration/expected/test_PiwikTracker_trackForceUsingVisitId_insteadOfHeuristics_alsoTestsCampaignTracking__Referers.getCampaigns_day.xml index 9b6c31b0559b7e019456bb29f2d10a8588f030a8..69520dea3d353860fcc1664093e64d57d73b74a7 100644 --- a/tests/integration/expected/test_PiwikTracker_trackForceUsingVisitId_insteadOfHeuristics_alsoTestsCampaignTracking__Referers.getCampaigns_day.xml +++ b/tests/integration/expected/test_PiwikTracker_trackForceUsingVisitId_insteadOfHeuristics_alsoTestsCampaignTracking__Referers.getCampaigns_day.xml @@ -12,11 +12,11 @@ <row idgoal='1'> <nb_conversions>1</nb_conversions> <nb_visits_converted>1</nb_visits_converted> - <revenue>42.25</revenue> + <revenue>42.26</revenue> </row> </goals> <nb_conversions>1</nb_conversions> - <revenue>42.25</revenue> + <revenue>42.26</revenue> <subtable> <row> <label>Piwik kwd</label> @@ -30,11 +30,11 @@ <row idgoal='1'> <nb_conversions>1</nb_conversions> <nb_visits_converted>1</nb_visits_converted> - <revenue>42.25</revenue> + <revenue>42.26</revenue> </row> </goals> <nb_conversions>1</nb_conversions> - <revenue>42.25</revenue> + <revenue>42.26</revenue> </row> </subtable> </row> diff --git a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_day.xml b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_day.xml index 12de83797fd7a192fc8cd7ce1d3e0a597e0b1b04..e91916f991db56b8f95170e38872e5fd530f59bc 100644 --- a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_day.xml +++ b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_day.xml @@ -27,11 +27,9 @@ <nb_uniq_visitors>1</nb_uniq_visitors> <nb_hits>1</nb_hits> <sum_time_spent>0</sum_time_spent> - <exit_nb_uniq_visitors>1</exit_nb_uniq_visitors> - <exit_nb_visits>1</exit_nb_visits> <avg_time_on_page>0</avg_time_on_page> <bounce_rate>0%</bounce_rate> - <exit_rate>100%</exit_rate> + <exit_rate>0%</exit_rate> </row> </result> <result date="2010-01-05"> diff --git a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_month.xml b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_month.xml index e0dd626f42f6e123d2788637baf6f9f0a838ee00..dcfde2bd995c6df8f2e74aefb801a41e8578123e 100644 --- a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_month.xml +++ b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_month.xml @@ -79,12 +79,10 @@ <nb_visits>1</nb_visits> <nb_hits>1</nb_hits> <sum_time_spent>0</sum_time_spent> - <exit_nb_visits>1</exit_nb_visits> <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> - <sum_daily_exit_nb_uniq_visitors>1</sum_daily_exit_nb_uniq_visitors> <avg_time_on_page>0</avg_time_on_page> <bounce_rate>0%</bounce_rate> - <exit_rate>100%</exit_rate> + <exit_rate>0%</exit_rate> </row> </result> <result date="2010-02" /> diff --git a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_week.xml b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_week.xml index 5ad111fece393d4f416d33e82e25d81a9e35f4f4..11c2526451ef9570570e1a7c582adffa472ce592 100644 --- a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_week.xml +++ b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_week.xml @@ -81,12 +81,10 @@ <nb_visits>1</nb_visits> <nb_hits>1</nb_hits> <sum_time_spent>0</sum_time_spent> - <exit_nb_visits>1</exit_nb_visits> <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> - <sum_daily_exit_nb_uniq_visitors>1</sum_daily_exit_nb_uniq_visitors> <avg_time_on_page>0</avg_time_on_page> <bounce_rate>0%</bounce_rate> - <exit_rate>100%</exit_rate> + <exit_rate>0%</exit_rate> </row> </result> <result date="2010-01-11 to 2010-01-17" /> diff --git a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_year.xml b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_year.xml index 5776b676b7347a8a7f285ad246efdb1ea7007f43..5ae46a54f32dc29e2adec790591c2eb563157180 100644 --- a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_year.xml +++ b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageTitles_year.xml @@ -79,12 +79,10 @@ <nb_visits>1</nb_visits> <nb_hits>1</nb_hits> <sum_time_spent>0</sum_time_spent> - <exit_nb_visits>1</exit_nb_visits> <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> - <sum_daily_exit_nb_uniq_visitors>1</sum_daily_exit_nb_uniq_visitors> <avg_time_on_page>0</avg_time_on_page> <bounce_rate>0%</bounce_rate> - <exit_rate>100%</exit_rate> + <exit_rate>0%</exit_rate> </row> </result> <result date="2011" /> diff --git a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_day.xml b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_day.xml index fea2843acc7e73507451b4d719d04efac60f19fa..92cc681d9d852f2d63c131db50c06c3f25ec366f 100644 --- a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_day.xml +++ b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_day.xml @@ -39,11 +39,9 @@ <nb_uniq_visitors>1</nb_uniq_visitors> <nb_hits>1</nb_hits> <sum_time_spent>0</sum_time_spent> - <exit_nb_uniq_visitors>1</exit_nb_uniq_visitors> - <exit_nb_visits>1</exit_nb_visits> <avg_time_on_page>0</avg_time_on_page> <bounce_rate>0%</bounce_rate> - <exit_rate>100%</exit_rate> + <exit_rate>0%</exit_rate> <url></url> </row> </result> diff --git a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_month.xml b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_month.xml index 6b29877f2b7b63c877b34466c173e7cd84c87bae..b9d1acb081fec9fa82b6e06c93ee5747f2d9b822 100644 --- a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_month.xml +++ b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_month.xml @@ -52,12 +52,10 @@ <nb_visits>1</nb_visits> <nb_hits>1</nb_hits> <sum_time_spent>0</sum_time_spent> - <exit_nb_visits>1</exit_nb_visits> <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> - <sum_daily_exit_nb_uniq_visitors>1</sum_daily_exit_nb_uniq_visitors> <avg_time_on_page>0</avg_time_on_page> <bounce_rate>0%</bounce_rate> - <exit_rate>100%</exit_rate> + <exit_rate>0%</exit_rate> <url></url> </row> </result> diff --git a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_week.xml b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_week.xml index 9225c807cc690bfbcb6189770a3d858cb134ad5e..a6916d10584f37cb650d75098fc785b04922c3d5 100644 --- a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_week.xml +++ b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_week.xml @@ -65,12 +65,10 @@ <nb_visits>1</nb_visits> <nb_hits>1</nb_hits> <sum_time_spent>0</sum_time_spent> - <exit_nb_visits>1</exit_nb_visits> <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> - <sum_daily_exit_nb_uniq_visitors>1</sum_daily_exit_nb_uniq_visitors> <avg_time_on_page>0</avg_time_on_page> <bounce_rate>0%</bounce_rate> - <exit_rate>100%</exit_rate> + <exit_rate>0%</exit_rate> <url></url> </row> </result> diff --git a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_year.xml b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_year.xml index 0ce97c43b0b80e567f8d67afb76c563c8d7cdfe9..19b419e61f507cabb3a9b7a1451ff5e18649c79d 100644 --- a/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_year.xml +++ b/tests/integration/expected/test_TwoVisitors_twoWebsites_differentDays__Actions.getPageUrls_year.xml @@ -52,12 +52,10 @@ <nb_visits>1</nb_visits> <nb_hits>1</nb_hits> <sum_time_spent>0</sum_time_spent> - <exit_nb_visits>1</exit_nb_visits> <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> - <sum_daily_exit_nb_uniq_visitors>1</sum_daily_exit_nb_uniq_visitors> <avg_time_on_page>0</avg_time_on_page> <bounce_rate>0%</bounce_rate> - <exit_rate>100%</exit_rate> + <exit_rate>0%</exit_rate> <url></url> </row> </result> diff --git a/tests/integration/expected/test_apiGetReportMetadata__API.getMetadata.xml b/tests/integration/expected/test_apiGetReportMetadata__API.getMetadata.xml deleted file mode 100644 index 1c8ffd506bd7829467ccfafdcbc8a5f399db4e6b..0000000000000000000000000000000000000000 --- a/tests/integration/expected/test_apiGetReportMetadata__API.getMetadata.xml +++ /dev/null @@ -1,36 +0,0 @@ -<?xml version="1.0" encoding="utf-8" ?> -<result> - <row> - <category>Visitors</category> - <name>Country</name> - <module>UserCountry</module> - <action>getCountry</action> - <dimension>Country</dimension> - <metrics> - <nb_visits>Visits</nb_visits> - <nb_uniq_visitors>Unique visitors</nb_uniq_visitors> - <nb_actions>Actions</nb_actions> - - </metrics> - <processedMetrics> - <nb_actions_per_visit>Actions per Visit</nb_actions_per_visit> - <avg_time_on_site>Avg. Time on Website</avg_time_on_site> - <bounce_rate>Bounce Rate</bounce_rate> - <conversion_rate>Conversion Rate</conversion_rate> - - </processedMetrics> - <metricsGoal> - <nb_conversions>Conversions</nb_conversions> - <conversion_rate>Conversion Rate</conversion_rate> - <revenue>Revenue</revenue> - - </metricsGoal> - <processedMetricsGoal> - <revenue_per_visit>Value per Visit</revenue_per_visit> - - </processedMetricsGoal> - <uniqueId>UserCountry_getCountry</uniqueId> - - </row> - -</result> \ No newline at end of file diff --git a/tests/integration/expected/test_apiGetReportMetadata__API.getProcessedReport_day.xml b/tests/integration/expected/test_apiGetReportMetadata__API.getProcessedReport_day.xml index 0ac04890708580d40dd7e0d7a0b45e935e9dbdb9..a9b9650936680e5191902bc253c5bc01d2f208d0 100644 --- a/tests/integration/expected/test_apiGetReportMetadata__API.getProcessedReport_day.xml +++ b/tests/integration/expected/test_apiGetReportMetadata__API.getProcessedReport_day.xml @@ -62,7 +62,7 @@ <nb_uniq_visitors>1</nb_uniq_visitors> <nb_visits>1</nb_visits> <nb_actions>1</nb_actions> - <revenue>$ 42.25</revenue> + <revenue>$ 42.26</revenue> <nb_actions_per_visit>1</nb_actions_per_visit> <avg_time_on_site>00:18:00</avg_time_on_site> <bounce_rate>100%</bounce_rate> diff --git a/tests/integration/expected/test_apiGetReportMetadata__API.getSegmentsMetadata.xml b/tests/integration/expected/test_apiGetReportMetadata__API.getSegmentsMetadata.xml index 94659d9ff4b22b1ba2df997bd101964d9f95c8dc..9bcc275e38f3023404b0246c7ff2c9ca23ed7aec 100644 --- a/tests/integration/expected/test_apiGetReportMetadata__API.getSegmentsMetadata.xml +++ b/tests/integration/expected/test_apiGetReportMetadata__API.getSegmentsMetadata.xml @@ -7,6 +7,13 @@ <segment>visitConverted</segment> <acceptedValues>0, 1</acceptedValues> </row> + <row> + <type>metric</type> + <category>Visit</category> + <name>Visit Ecommerce status at the end of the visit. For example, to select all visits that have made an Ecommerce order, the API request would contain "&segment=visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart"</name> + <segment>visitEcommerceStatus</segment> + <acceptedValues>none, ordered, abandonedCart, orderedThenAbandonedCart</acceptedValues> + </row> <row> <type>metric</type> <category>Visit</category> @@ -31,6 +38,12 @@ <name>Days since last visit</name> <segment>daysSinceLastVisit</segment> </row> + <row> + <type>metric</type> + <category>Visit</category> + <name>Days since last Ecommerce order</name> + <segment>daysSinceLastEcommerceOrder</segment> + </row> <row> <type>metric</type> <category>Visit</category> diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..53a82090efb1cad986c807f7b2bdfd08d9a8edcd --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_day.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Category TWO LEFT in cart</label> + <revenue>4000</revenue> + <quantity>4.00</quantity> + <abandoned_carts>2</abandoned_carts> + <avg_price>1000</avg_price> + <avg_quantity>2</avg_quantity> + </row> + <row> + <label>Product Category not defined</label> + <revenue>1000.22</revenue> + <quantity>2.00</quantity> + <abandoned_carts>2</abandoned_carts> + <avg_price>500.11</avg_price> + <avg_quantity>1</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..263c329f3e86e02268ec2bb32a34c250253631c0 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_week.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Category TWO LEFT in cart</label> + <revenue>6000</revenue> + <quantity>6</quantity> + <abandoned_carts>3</abandoned_carts> + <avg_price>1000</avg_price> + <avg_quantity>2</avg_quantity> + </row> + <row> + <label>Product Category not defined</label> + <revenue>1500.33</revenue> + <quantity>3</quantity> + <abandoned_carts>3</abandoned_carts> + <avg_price>500.11</avg_price> + <avg_quantity>1</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c3b0b3a35840b470d637bba439905ac6ba575af5 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_day.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>PRODUCT TWO LEFT in cart</label> + <revenue>4000</revenue> + <quantity>4.00</quantity> + <abandoned_carts>2</abandoned_carts> + <avg_price>1000</avg_price> + <avg_quantity>2</avg_quantity> + </row> + <row> + <label>PRODUCT ONE LEFT in cart</label> + <revenue>1000.22</revenue> + <quantity>2.00</quantity> + <abandoned_carts>2</abandoned_carts> + <avg_price>500.11</avg_price> + <avg_quantity>1</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..1d603853ae14dc92ae72e94b0ba65e81f2101249 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_week.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>PRODUCT TWO LEFT in cart</label> + <revenue>6000</revenue> + <quantity>6</quantity> + <abandoned_carts>3</abandoned_carts> + <avg_price>1000</avg_price> + <avg_quantity>2</avg_quantity> + </row> + <row> + <label>PRODUCT ONE LEFT in cart</label> + <revenue>1500.33</revenue> + <quantity>3</quantity> + <abandoned_carts>3</abandoned_carts> + <avg_price>500.11</avg_price> + <avg_quantity>1</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..2d73a799ff8bf28f10dc2c11ca132df823b608c8 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_day.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>SKU IN ABANDONED CART TWO</label> + <revenue>4000</revenue> + <quantity>4.00</quantity> + <abandoned_carts>2</abandoned_carts> + <avg_price>1000</avg_price> + <avg_quantity>2</avg_quantity> + </row> + <row> + <label>SKU IN ABANDONED CART ONE</label> + <revenue>1000.22</revenue> + <quantity>2.00</quantity> + <abandoned_carts>2</abandoned_carts> + <avg_price>500.11</avg_price> + <avg_quantity>1</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..59fd6ae2eff7866a922e2f1db4b5e0d1010acf8a --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_week.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>SKU IN ABANDONED CART TWO</label> + <revenue>6000</revenue> + <quantity>6</quantity> + <abandoned_carts>3</abandoned_carts> + <avg_price>1000</avg_price> + <avg_quantity>2</avg_quantity> + </row> + <row> + <label>SKU IN ABANDONED CART ONE</label> + <revenue>1500.33</revenue> + <quantity>3</quantity> + <abandoned_carts>3</abandoned_carts> + <avg_price>500.11</avg_price> + <avg_quantity>1</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..693e7dbc3235c99ae4104c1f7238402917281e82 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_day.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>2</nb_conversions> + <nb_visits_converted>2</nb_visits_converted> + <conversion_rate>100</conversion_rate> + <revenue>5000.22</revenue> + <items>6</items> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..5c8effaf28e60e69969c70378ca6379eff4e4688 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_week.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>3</nb_conversions> + <nb_visits_converted>3</nb_visits_converted> + <conversion_rate>75</conversion_rate> + <revenue>7500.33</revenue> + <items>9</items> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..fc62e5e6a5df74ba0f66b23a081d9a783f543ceb --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_day.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>1</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <conversion_rate>50</conversion_rate> + <revenue>10</revenue> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..c29e706e50d3f269af6de0edf48709672021bc39 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_week.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>1</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <conversion_rate>25</conversion_rate> + <revenue>10</revenue> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..857aa10333bfa7ef8c2649543ffbab8f50b5655f --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_day.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>2</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <conversion_rate>50</conversion_rate> + <revenue>3111.11</revenue> + <revenue_subtotal>2500</revenue_subtotal> + <revenue_tax>511</revenue_tax> + <revenue_shipping>100.11</revenue_shipping> + <revenue_discount>666</revenue_discount> + <items>3</items> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..af971d80480f5f73afbcf55b45206b77ae9070e9 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_week.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>4</nb_conversions> + <nb_visits_converted>2</nb_visits_converted> + <conversion_rate>50</conversion_rate> + <revenue>13351.1</revenue> + <revenue_subtotal>2700</revenue_subtotal> + <revenue_tax>531</revenue_tax> + <revenue_shipping>120.11</revenue_shipping> + <revenue_discount>686</revenue_discount> + <items>5</items> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..5867f1dd24b8bf86d7b9a1dbec8aa9dbf34c1143 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_day.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>3</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <conversion_rate>50</conversion_rate> + <revenue>3121.11</revenue> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..3b8f44347024ce2acfebe903b6f08a70336f3b30 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_week.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>5</nb_conversions> + <nb_visits_converted>3</nb_visits_converted> + <conversion_rate>75</conversion_rate> + <revenue>13361.1</revenue> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getLastVisitsForVisitor.xml b/tests/integration/expected/test_ecommerceOrderWithItems_LiveEcommerceStatusOrdered__Live.getLastVisitsDetails_day.xml similarity index 55% rename from tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getLastVisitsForVisitor.xml rename to tests/integration/expected/test_ecommerceOrderWithItems_LiveEcommerceStatusOrdered__Live.getLastVisitsDetails_day.xml index 9a016901b2e642701b0702d735c8bc77d42dc28c..d2520843fa87c919a09ca3562904ee665d90cb63 100644 --- a/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getLastVisitsForVisitor.xml +++ b/tests/integration/expected/test_ecommerceOrderWithItems_LiveEcommerceStatusOrdered__Live.getLastVisitsDetails_day.xml @@ -2,31 +2,38 @@ <result> <row> <idSite>1</idSite> - <idVisit>3</idVisit> + <idVisit>4</idVisit> <visitIp>156.5.3.2</visitIp> <visitorType>returning</visitorType> - <visitConverted>0</visitConverted> + <visitConverted>1</visitConverted> + <visitEcommerceStatus>ordered</visitEcommerceStatus> <actions>1</actions> <actionDetails> <row> - <type>outlink</type> - <url>http://test.com</url> - <pageTitle></pageTitle> - <pageIdAction>5</pageIdAction> - <pageId>4</pageId> + <type>ecommerceOrder</type> + <orderId>666</orderId> + <revenue>240</revenue> + <revenueSubTotal>200</revenueSubTotal> + <revenueTax>20</revenueTax> + <revenueShipping>20</revenueShipping> + <revenueDiscount>20</revenueDiscount> + <items>2</items> </row> - </actionDetails> - <customVariables> - <row> - <customVariableName1>VisitorType</customVariableName1> - <customVariableValue1>LoggedOut</customVariableValue1> - </row> <row> - <customVariableName2>Othercustom value which should be truncated abcdef</customVariableName2> - <customVariableValue2>abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx</customVariableValue2> + <type>ecommerceOrder</type> + <orderId>777</orderId> + <revenue>10000</revenue> + <revenueSubTotal>0</revenueSubTotal> + <revenueTax>0</revenueTax> + <revenueShipping>0</revenueShipping> + <revenueDiscount>0</revenueDiscount> + <items>0</items> + </row> + </actionDetails> + <customVariables> </customVariables> <goalConversions>0</goalConversions> <siteCurrency>USD</siteCurrency> @@ -35,11 +42,12 @@ - <visitDuration>0</visitDuration> - <visitDurationPretty>0s</visitDurationPretty> + <visitDuration>360</visitDuration> + <visitDurationPretty>6 min 0s</visitDurationPretty> <visitCount>1</visitCount> <daysSinceLastVisit>0</daysSinceLastVisit> <daysSinceFirstVisit>0</daysSinceFirstVisit> + <daysSinceLastEcommerceOrder>0</daysSinceLastEcommerceOrder> <country>France</country> <countryFlag>plugins/UserCountry/flags/fr.png</countryFlag> <continent>Europe</continent> @@ -47,10 +55,10 @@ <providerUrl>http://piwik.org/faq/general/#faq_52</providerUrl> <referrerType>direct</referrerType> <referrerTypeName>Direct Entry</referrerTypeName> + <referrerName></referrerName> <referrerKeyword></referrerKeyword> <referrerKeywordPosition></referrerKeywordPosition> <referrerUrl></referrerUrl> - <referrerName></referrerName> <referrerSearchEngineUrl></referrerSearchEngineUrl> <referrerSearchEngineIcon></referrerSearchEngineIcon> <operatingSystem>Windows XP</operatingSystem> @@ -58,11 +66,11 @@ <operatingSystemIcon>plugins/UserSettings/images/os/WXP.gif</operatingSystemIcon> <browserFamily>gecko</browserFamily> <browserFamilyDescription>Gecko (Firefox)</browserFamilyDescription> - <browserName>Firefox 3.0</browserName> + <browserName>Firefox 3.6</browserName> <browserIcon>plugins/UserSettings/images/browsers/FF.gif</browserIcon> - <screenType>dual</screenType> - <resolution>1111x222</resolution> - <screenTypeIcon>plugins/UserSettings/images/screens/dual.gif</screenTypeIcon> + <screenType>normal</screenType> + <resolution>1024x768</resolution> + <screenTypeIcon>plugins/UserSettings/images/screens/normal.gif</screenTypeIcon> <plugins>flash, java</plugins> <pluginsIcons> <row> @@ -79,36 +87,62 @@ + <ecommerce> + <row> + <type>ecommerceOrder</type> + <orderId>666</orderId> + <revenue>240</revenue> + <revenueSubTotal>200</revenueSubTotal> + <revenueTax>20</revenueTax> + <revenueShipping>20</revenueShipping> + <revenueDiscount>20</revenueDiscount> + <items>2</items> + + <itemDetails> + <row> + <itemSKU>TRIPOD SKU</itemSKU> + <itemName>TRIPOD - bought day after</itemName> + <itemCategory>Tools</itemCategory> + <price>100</price> + <quantity>2</quantity> + </row> + </itemDetails> + </row> + <row> + <type>ecommerceOrder</type> + <orderId>777</orderId> + <revenue>10000</revenue> + <revenueSubTotal>0</revenueSubTotal> + <revenueTax>0</revenueTax> + <revenueShipping>0</revenueShipping> + <revenueDiscount>0</revenueDiscount> + <items>0</items> + + <itemDetails> + </itemDetails> + </row> + </ecommerce> </row> <row> <idSite>1</idSite> - <idVisit>2</idVisit> + <idVisit>3</idVisit> <visitIp>156.5.3.2</visitIp> - <visitorType>new</visitorType> + <visitorType>returning</visitorType> <visitConverted>1</visitConverted> + <visitEcommerceStatus>orderedThenAbandonedCart</visitEcommerceStatus> <actions>1</actions> <actionDetails> <row> - <type>goal</type> - <goalName>triggered js</goalName> - <revenue>0</revenue> - <goalPageId></goalPageId> + <type>ecommerceAbandonedCart</type> + <revenue>2500.11</revenue> + <items>3</items> - <url>http://example.org/homepage</url> </row> </actionDetails> <customVariables> - <row> - <customVariableName1>VisitorType</customVariableName1> - <customVariableValue1>LoggedOut</customVariableValue1> - </row> - <row> - <customVariableName2>Othercustom value which should be truncated abcdef</customVariableName2> - <customVariableValue2>abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx</customVariableValue2> - </row> </customVariables> - <goalConversions>1</goalConversions> + <goalConversions>0</goalConversions> <siteCurrency>USD</siteCurrency> <visitLocalTime>12:34:06</visitLocalTime> @@ -120,6 +154,7 @@ <visitCount>1</visitCount> <daysSinceLastVisit>0</daysSinceLastVisit> <daysSinceFirstVisit>0</daysSinceFirstVisit> + <daysSinceLastEcommerceOrder>0</daysSinceLastEcommerceOrder> <country>France</country> <countryFlag>plugins/UserCountry/flags/fr.png</countryFlag> <continent>Europe</continent> @@ -127,10 +162,10 @@ <providerUrl>http://piwik.org/faq/general/#faq_52</providerUrl> <referrerType>direct</referrerType> <referrerTypeName>Direct Entry</referrerTypeName> + <referrerName></referrerName> <referrerKeyword></referrerKeyword> <referrerKeywordPosition></referrerKeywordPosition> <referrerUrl></referrerUrl> - <referrerName></referrerName> <referrerSearchEngineUrl></referrerSearchEngineUrl> <referrerSearchEngineIcon></referrerSearchEngineIcon> <operatingSystem>Windows XP</operatingSystem> @@ -138,11 +173,11 @@ <operatingSystemIcon>plugins/UserSettings/images/os/WXP.gif</operatingSystemIcon> <browserFamily>gecko</browserFamily> <browserFamilyDescription>Gecko (Firefox)</browserFamilyDescription> - <browserName>Firefox 3.0</browserName> + <browserName>Firefox 3.6</browserName> <browserIcon>plugins/UserSettings/images/browsers/FF.gif</browserIcon> - <screenType>dual</screenType> - <resolution>1111x222</resolution> - <screenTypeIcon>plugins/UserSettings/images/screens/dual.gif</screenTypeIcon> + <screenType>normal</screenType> + <resolution>1024x768</resolution> + <screenTypeIcon>plugins/UserSettings/images/screens/normal.gif</screenTypeIcon> <plugins>flash, java</plugins> <pluginsIcons> <row> @@ -159,5 +194,29 @@ + <ecommerce> + <row> + <type>ecommerceAbandonedCart</type> + <revenue>2500.11</revenue> + <items>3</items> + + <itemDetails> + <row> + <itemSKU>SKU IN ABANDONED CART ONE</itemSKU> + <itemName>PRODUCT ONE LEFT in cart</itemName> + <itemCategory></itemCategory> + <price>500.11</price> + <quantity>1</quantity> + </row> + <row> + <itemSKU>SKU IN ABANDONED CART TWO</itemSKU> + <itemName>PRODUCT TWO LEFT in cart</itemName> + <itemCategory>Category TWO LEFT in cart</itemCategory> + <price>1000</price> + <quantity>2</quantity> + </row> + </itemDetails> + </row> + </ecommerce> </row> </result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_SegmentAbandonedCart__VisitsSummary.get_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_SegmentAbandonedCart__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..e8e1fa23a484b209e7a70c21cd592f7d1555095e --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_SegmentAbandonedCart__VisitsSummary.get_day.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_visits>2</nb_visits> + <nb_uniq_visitors>1</nb_uniq_visitors> + <nb_actions>2</nb_actions> + <nb_visits_converted>1</nb_visits_converted> + <bounce_count>2</bounce_count> + <sum_visit_length>2340</sum_visit_length> + <max_actions>1</max_actions> + <bounce_rate>100%</bounce_rate> + <nb_actions_per_visit>1</nb_actions_per_visit> + <avg_time_on_site>1170</avg_time_on_site> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_SegmentNoEcommerce__VisitsSummary.get_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_SegmentNoEcommerce__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..a4933d174cc5e36fd57dc0b4cd903f7a83646556 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_SegmentNoEcommerce__VisitsSummary.get_day.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_visits>0</nb_visits> + <nb_uniq_visitors>0</nb_uniq_visitors> + <nb_actions>0</nb_actions> + <nb_visits_converted>0</nb_visits_converted> + <bounce_count>0</bounce_count> + <sum_visit_length>0</sum_visit_length> + <max_actions>0</max_actions> + <bounce_rate>0%</bounce_rate> + <nb_actions_per_visit>0</nb_actions_per_visit> + <avg_time_on_site>0</avg_time_on_site> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems_SegmentOrderedSomething__VisitsSummary.get_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems_SegmentOrderedSomething__VisitsSummary.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..95b8e6c66735855faf3e982c5ec3aed29d13a990 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems_SegmentOrderedSomething__VisitsSummary.get_day.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_visits>1</nb_visits> + <nb_uniq_visitors>1</nb_uniq_visitors> + <nb_actions>1</nb_actions> + <nb_visits_converted>1</nb_visits_converted> + <bounce_count>1</bounce_count> + <sum_visit_length>2340</sum_visit_length> + <max_actions>1</max_actions> + <bounce_rate>100%</bounce_rate> + <nb_actions_per_visit>1</nb_actions_per_visit> + <avg_time_on_site>2340</avg_time_on_site> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__API.getProcessedReport_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__API.getProcessedReport_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..959c03d202aceb3f4db042df3f385d5d155d20cf --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__API.getProcessedReport_day.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <website>Piwik test</website> + <prettyDate>Tuesday 5 April 2011</prettyDate> + <metadata> + <category>Visitors</category> + <name>Country</name> + <module>UserCountry</module> + <action>getCountry</action> + <dimension>Country</dimension> + <metrics> + <nb_visits>Visits</nb_visits> + <nb_uniq_visitors>Unique visitors</nb_uniq_visitors> + <nb_actions>Actions</nb_actions> + + </metrics> + <processedMetrics> + <nb_actions_per_visit>Actions per Visit</nb_actions_per_visit> + <avg_time_on_site>Avg. Time on Website</avg_time_on_site> + <bounce_rate>Bounce Rate</bounce_rate> + + </processedMetrics> + <metricsDocumentation> + <nb_visits>If a visitor comes to your website for the first time or if he visits a page more than 30 minutes after his last page view, this will be recorded as a new visit.</nb_visits> + <nb_uniq_visitors>The number of unduplicated visitors coming to your website. Every user is only counted once, even if he visits the website multiple times a day.</nb_uniq_visitors> + <nb_actions>The number of actions performed by your visitors. Actions can be page views, downloads or outlinks.</nb_actions> + <nb_actions_per_visit>The average number of actions (page views, downloads or outlinks) that were performed during the visits.</nb_actions_per_visit> + <avg_time_on_site>The average duration of a visit.</avg_time_on_site> + <bounce_rate>The percentage of visits that only had a single pageview. This means, that the visitor left the website directly from the entrance page.</bounce_rate> + <conversion_rate>The percentage of visits that triggered a goal conversion.</conversion_rate> + <avg_time_on_page>The average amount of time visitors spent on this page (only the page, not the entire website).</avg_time_on_page> + <nb_hits>The number of times this page was visited.</nb_hits> + <exit_rate>The percentage of visits that left the website after viewing this page (unique pageviews devided by exists)</exit_rate> + + </metricsDocumentation> + <metricsGoal> + <nb_conversions>Conversions</nb_conversions> + <revenue>Revenue</revenue> + + </metricsGoal> + <processedMetricsGoal> + <revenue_per_visit>Revenue per Visit</revenue_per_visit> + + </processedMetricsGoal> + <uniqueId>UserCountry_getCountry</uniqueId> + + </metadata> + <columns> + <label>Country</label> + <nb_visits>Visits</nb_visits> + <nb_uniq_visitors>Unique visitors</nb_uniq_visitors> + <nb_actions>Actions</nb_actions> + <nb_actions_per_visit>Actions per Visit</nb_actions_per_visit> + <avg_time_on_site>Avg. Time on Website</avg_time_on_site> + <bounce_rate>Bounce Rate</bounce_rate> + <revenue>Revenue</revenue> + + </columns> + <reportData> + <row> + <label>France</label> + <nb_uniq_visitors>1</nb_uniq_visitors> + <nb_visits>2</nb_visits> + <nb_actions>2</nb_actions> + <revenue>$ 3121.11</revenue> + <nb_actions_per_visit>1</nb_actions_per_visit> + <avg_time_on_site>00:19:30</avg_time_on_site> + <bounce_rate>100%</bounce_rate> + + </row> + + </reportData> + <reportMetadata> + <row> + <code>fr</code> + <logo>plugins/UserCountry/flags/fr.png</logo> + <logoWidth>18</logoWidth> + <logoHeight>12</logoHeight> + + </row> + + </reportMetadata> + +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getConversions_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getConversions_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..15ef03fb49cfea4767aa035a031e96c3b348bc93 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getConversions_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result>3</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..0112cf98d43d36d59a260eab4591fccd3dbc9e48 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_day.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Electronics & Cameras</label> + <revenue>2500</revenue> + <quantity>3.00</quantity> + <orders>2</orders> + <avg_price>1000</avg_price> + <avg_quantity>1.5</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..84a8f9e8998b04355e4a5784f10e703685def197 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_week.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Electronics & Cameras</label> + <revenue>2500</revenue> + <quantity>3.00</quantity> + <orders>2</orders> + <avg_price>1000</avg_price> + <avg_quantity>1.5</avg_quantity> + </row> + <row> + <label>Tools</label> + <revenue>200</revenue> + <quantity>2.00</quantity> + <orders>1</orders> + <avg_price>100</avg_price> + <avg_quantity>2</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsName_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..d72196172abd0d64702ee375e21cc241e570ffa7 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsName_day.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Canon SLR</label> + <revenue>1500</revenue> + <quantity>1.00</quantity> + <orders>1</orders> + <avg_price>1500</avg_price> + <avg_quantity>1</avg_quantity> + </row> + <row> + <label>PRODUCT name</label> + <revenue>1000</revenue> + <quantity>2.00</quantity> + <orders>1</orders> + <avg_price>500</avg_price> + <avg_quantity>2</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsName_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsName_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..4199ef33379a7564ba14c98d145dddf3f70f00dc --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsName_week.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Canon SLR</label> + <revenue>1500</revenue> + <quantity>1.00</quantity> + <orders>1</orders> + <avg_price>1500</avg_price> + <avg_quantity>1</avg_quantity> + </row> + <row> + <label>PRODUCT name</label> + <revenue>1000</revenue> + <quantity>2.00</quantity> + <orders>1</orders> + <avg_price>500</avg_price> + <avg_quantity>2</avg_quantity> + </row> + <row> + <label>TRIPOD - bought day after</label> + <revenue>200</revenue> + <quantity>2.00</quantity> + <orders>1</orders> + <avg_price>100</avg_price> + <avg_quantity>2</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..43bc239c7121bdeb75554c94b05317364bd75396 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_day.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>SKU2</label> + <revenue>1500</revenue> + <quantity>1.00</quantity> + <orders>1</orders> + <avg_price>1500</avg_price> + <avg_quantity>1</avg_quantity> + </row> + <row> + <label>SKU VERY nice indeed</label> + <revenue>1000</revenue> + <quantity>2.00</quantity> + <orders>1</orders> + <avg_price>500</avg_price> + <avg_quantity>2</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..6263163fd88a4c003bd5a6e76bcb00e7d6564d99 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_week.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>SKU2</label> + <revenue>1500</revenue> + <quantity>1.00</quantity> + <orders>1</orders> + <avg_price>1500</avg_price> + <avg_quantity>1</avg_quantity> + </row> + <row> + <label>SKU VERY nice indeed</label> + <revenue>1000</revenue> + <quantity>2.00</quantity> + <orders>1</orders> + <avg_price>500</avg_price> + <avg_quantity>2</avg_quantity> + </row> + <row> + <label>TRIPOD SKU</label> + <revenue>200</revenue> + <quantity>2.00</quantity> + <orders>1</orders> + <avg_price>100</avg_price> + <avg_quantity>2</avg_quantity> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.get_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.get_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..5867f1dd24b8bf86d7b9a1dbec8aa9dbf34c1143 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.get_day.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>3</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <conversion_rate>50</conversion_rate> + <revenue>3121.11</revenue> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Goals.get_week.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.get_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..3b8f44347024ce2acfebe903b6f08a70336f3b30 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Goals.get_week.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_conversions>5</nb_conversions> + <nb_visits_converted>3</nb_visits_converted> + <conversion_rate>75</conversion_rate> + <revenue>13361.1</revenue> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__Live.getLastVisitsDetails_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__Live.getLastVisitsDetails_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..7fddc2974fc30828f50474d22ed7d49f0433233e --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__Live.getLastVisitsDetails_day.xml @@ -0,0 +1,273 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <idSite>1</idSite> + <idVisit>2</idVisit> + <visitIp>156.5.3.2</visitIp> + + <visitorType>returning</visitorType> + <visitConverted>0</visitConverted> + <visitEcommerceStatus>abandonedCart</visitEcommerceStatus> + <actions>1</actions> + <actionDetails> + <row> + <type>ecommerceAbandonedCart</type> + <revenue>2500.11</revenue> + <items>3</items> + + </row> + </actionDetails> + <customVariables> + </customVariables> + <goalConversions>0</goalConversions> + <siteCurrency>USD</siteCurrency> + + <visitLocalTime>12:34:06</visitLocalTime> + + + + <visitDuration>0</visitDuration> + <visitDurationPretty>0s</visitDurationPretty> + <visitCount>1</visitCount> + <daysSinceLastVisit>0</daysSinceLastVisit> + <daysSinceFirstVisit>0</daysSinceFirstVisit> + <daysSinceLastEcommerceOrder>0</daysSinceLastEcommerceOrder> + <country>France</country> + <countryFlag>plugins/UserCountry/flags/fr.png</countryFlag> + <continent>Europe</continent> + <provider>Unknown</provider> + <providerUrl>http://piwik.org/faq/general/#faq_52</providerUrl> + <referrerType>direct</referrerType> + <referrerTypeName>Direct Entry</referrerTypeName> + <referrerName></referrerName> + <referrerKeyword></referrerKeyword> + <referrerKeywordPosition></referrerKeywordPosition> + <referrerUrl></referrerUrl> + <referrerSearchEngineUrl></referrerSearchEngineUrl> + <referrerSearchEngineIcon></referrerSearchEngineIcon> + <operatingSystem>Windows XP</operatingSystem> + <operatingSystemShortName>Win XP</operatingSystemShortName> + <operatingSystemIcon>plugins/UserSettings/images/os/WXP.gif</operatingSystemIcon> + <browserFamily>gecko</browserFamily> + <browserFamilyDescription>Gecko (Firefox)</browserFamilyDescription> + <browserName>Firefox 3.6</browserName> + <browserIcon>plugins/UserSettings/images/browsers/FF.gif</browserIcon> + <screenType>normal</screenType> + <resolution>1024x768</resolution> + <screenTypeIcon>plugins/UserSettings/images/screens/normal.gif</screenTypeIcon> + <plugins>flash, java</plugins> + <pluginsIcons> + <row> + <pluginIcon>plugins/UserSettings/images/plugins/flash.gif</pluginIcon> + <pluginName>flash</pluginName> + </row> + <row> + <pluginIcon>plugins/UserSettings/images/plugins/java.gif</pluginIcon> + <pluginName>java</pluginName> + </row> + </pluginsIcons> + + + + + + <ecommerce> + <row> + <type>ecommerceAbandonedCart</type> + <revenue>2500.11</revenue> + <items>3</items> + + <itemDetails> + <row> + <itemSKU>SKU IN ABANDONED CART ONE</itemSKU> + <itemName>PRODUCT ONE LEFT in cart</itemName> + <itemCategory></itemCategory> + <price>500.11</price> + <quantity>1</quantity> + </row> + <row> + <itemSKU>SKU IN ABANDONED CART TWO</itemSKU> + <itemName>PRODUCT TWO LEFT in cart</itemName> + <itemCategory>Category TWO LEFT in cart</itemCategory> + <price>1000</price> + <quantity>2</quantity> + </row> + </itemDetails> + </row> + </ecommerce> + </row> + <row> + <idSite>1</idSite> + <idVisit>1</idVisit> + <visitIp>156.5.3.2</visitIp> + + <visitorType>new</visitorType> + <visitConverted>1</visitConverted> + <visitEcommerceStatus>orderedThenAbandonedCart</visitEcommerceStatus> + <actions>1</actions> + <actionDetails> + <row> + <type>action</type> + <url>http://example.org/index.htm</url> + <pageTitle>incredible title!</pageTitle> + <pageIdAction>2</pageIdAction> + <pageId>1</pageId> + + </row> + <row> + <type>goal</type> + <goalName>triggered js ONCE</goalName> + <revenue>10</revenue> + <goalPageId>1</goalPageId> + + <url>http://example.org/index.htm</url> + </row> + <row> + <type>ecommerceOrder</type> + <orderId>937nsjusu 3894</orderId> + <revenue>1111.11</revenue> + <revenueSubTotal>1000</revenueSubTotal> + <revenueTax>111</revenueTax> + <revenueShipping>0.11</revenueShipping> + <revenueDiscount>666</revenueDiscount> + <items>2</items> + + </row> + <row> + <type>ecommerceOrder</type> + <orderId>1037nsjusu4s3894</orderId> + <revenue>2000</revenue> + <revenueSubTotal>1500</revenueSubTotal> + <revenueTax>400</revenueTax> + <revenueShipping>100</revenueShipping> + <revenueDiscount>0</revenueDiscount> + <items>1</items> + + </row> + <row> + <type>ecommerceAbandonedCart</type> + <revenue>2500.11</revenue> + <items>3</items> + + </row> + </actionDetails> + <customVariables> + </customVariables> + <goalConversions>1</goalConversions> + <siteCurrency>USD</siteCurrency> + + <visitLocalTime>12:34:06</visitLocalTime> + + + + <visitDuration>2340</visitDuration> + <visitDurationPretty>39 min 0s</visitDurationPretty> + <visitCount>1</visitCount> + <daysSinceLastVisit>0</daysSinceLastVisit> + <daysSinceFirstVisit>0</daysSinceFirstVisit> + <daysSinceLastEcommerceOrder>0</daysSinceLastEcommerceOrder> + <country>France</country> + <countryFlag>plugins/UserCountry/flags/fr.png</countryFlag> + <continent>Europe</continent> + <provider>Unknown</provider> + <providerUrl>http://piwik.org/faq/general/#faq_52</providerUrl> + <referrerType>direct</referrerType> + <referrerTypeName>Direct Entry</referrerTypeName> + <referrerName></referrerName> + <referrerKeyword></referrerKeyword> + <referrerKeywordPosition></referrerKeywordPosition> + <referrerUrl></referrerUrl> + <referrerSearchEngineUrl></referrerSearchEngineUrl> + <referrerSearchEngineIcon></referrerSearchEngineIcon> + <operatingSystem>Windows XP</operatingSystem> + <operatingSystemShortName>Win XP</operatingSystemShortName> + <operatingSystemIcon>plugins/UserSettings/images/os/WXP.gif</operatingSystemIcon> + <browserFamily>gecko</browserFamily> + <browserFamilyDescription>Gecko (Firefox)</browserFamilyDescription> + <browserName>Firefox 3.6</browserName> + <browserIcon>plugins/UserSettings/images/browsers/FF.gif</browserIcon> + <screenType>normal</screenType> + <resolution>1024x768</resolution> + <screenTypeIcon>plugins/UserSettings/images/screens/normal.gif</screenTypeIcon> + <plugins>flash, java</plugins> + <pluginsIcons> + <row> + <pluginIcon>plugins/UserSettings/images/plugins/flash.gif</pluginIcon> + <pluginName>flash</pluginName> + </row> + <row> + <pluginIcon>plugins/UserSettings/images/plugins/java.gif</pluginIcon> + <pluginName>java</pluginName> + </row> + </pluginsIcons> + + + + + + <ecommerce> + <row> + <type>ecommerceOrder</type> + <orderId>937nsjusu 3894</orderId> + <revenue>1111.11</revenue> + <revenueSubTotal>1000</revenueSubTotal> + <revenueTax>111</revenueTax> + <revenueShipping>0.11</revenueShipping> + <revenueDiscount>666</revenueDiscount> + <items>2</items> + + <itemDetails> + <row> + <itemSKU>SKU VERY nice indeed</itemSKU> + <itemName>PRODUCT name</itemName> + <itemCategory>Electronics & Cameras</itemCategory> + <price>500</price> + <quantity>2</quantity> + </row> + </itemDetails> + </row> + <row> + <type>ecommerceOrder</type> + <orderId>1037nsjusu4s3894</orderId> + <revenue>2000</revenue> + <revenueSubTotal>1500</revenueSubTotal> + <revenueTax>400</revenueTax> + <revenueShipping>100</revenueShipping> + <revenueDiscount>0</revenueDiscount> + <items>1</items> + + <itemDetails> + <row> + <itemSKU>SKU2</itemSKU> + <itemName>Canon SLR</itemName> + <itemCategory>Electronics & Cameras</itemCategory> + <price>1500</price> + <quantity>1</quantity> + </row> + </itemDetails> + </row> + <row> + <type>ecommerceAbandonedCart</type> + <revenue>2500.11</revenue> + <items>3</items> + + <itemDetails> + <row> + <itemSKU>SKU IN ABANDONED CART ONE</itemSKU> + <itemName>PRODUCT ONE LEFT in cart</itemName> + <itemCategory></itemCategory> + <price>500.11</price> + <quantity>1</quantity> + </row> + <row> + <itemSKU>SKU IN ABANDONED CART TWO</itemSKU> + <itemName>PRODUCT TWO LEFT in cart</itemName> + <itemCategory>Category TWO LEFT in cart</itemCategory> + <price>1000</price> + <quantity>2</quantity> + </row> + </itemDetails> + </row> + </ecommerce> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getContinent_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getContinent_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..f96445a3b91cbc9632befb5cf998f46b8ddd8160 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getContinent_day.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Europe</label> + <nb_uniq_visitors>1</nb_uniq_visitors> + <nb_visits>2</nb_visits> + <nb_actions>2</nb_actions> + <max_actions>1</max_actions> + <sum_visit_length>2340</sum_visit_length> + <bounce_count>2</bounce_count> + <goals> + <row idgoal='ecommerceAbandonedCart'> + <nb_conversions>2</nb_conversions> + <nb_visits_converted>2</nb_visits_converted> + <revenue>5000.22</revenue> + <items>6</items> + </row> + <row idgoal='ecommerceOrder'> + <nb_conversions>2</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <revenue>3111.11</revenue> + <revenue_subtotal>2500</revenue_subtotal> + <revenue_tax>511</revenue_tax> + <revenue_shipping>100.11</revenue_shipping> + <revenue_discount>666</revenue_discount> + <items>3</items> + </row> + <row idgoal='1'> + <nb_conversions>1</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <revenue>10</revenue> + </row> + </goals> + <nb_conversions>3</nb_conversions> + <revenue>3121.11</revenue> + <code>Europe</code> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getCountry_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getCountry_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..2509d485b5b581604797f540f3a841b258dc9d49 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getCountry_day.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>France</label> + <nb_uniq_visitors>1</nb_uniq_visitors> + <nb_visits>2</nb_visits> + <nb_actions>2</nb_actions> + <max_actions>1</max_actions> + <sum_visit_length>2340</sum_visit_length> + <bounce_count>2</bounce_count> + <goals> + <row idgoal='ecommerceAbandonedCart'> + <nb_conversions>2</nb_conversions> + <nb_visits_converted>2</nb_visits_converted> + <revenue>5000.22</revenue> + <items>6</items> + </row> + <row idgoal='ecommerceOrder'> + <nb_conversions>2</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <revenue>3111.11</revenue> + <revenue_subtotal>2500</revenue_subtotal> + <revenue_tax>511</revenue_tax> + <revenue_shipping>100.11</revenue_shipping> + <revenue_discount>666</revenue_discount> + <items>3</items> + </row> + <row idgoal='1'> + <nb_conversions>1</nb_conversions> + <nb_visits_converted>1</nb_visits_converted> + <revenue>10</revenue> + </row> + </goals> + <nb_conversions>3</nb_conversions> + <revenue>3121.11</revenue> + <code>fr</code> + <logo>plugins/UserCountry/flags/fr.png</logo> + <logoWidth>18</logoWidth> + <logoHeight>12</logoHeight> + </row> +</result> \ No newline at end of file diff --git a/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getNumberOfDistinctCountries_day.xml b/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getNumberOfDistinctCountries_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..606fbb524182170284d7f1baad7fce4697d9b8b3 --- /dev/null +++ b/tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getNumberOfDistinctCountries_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result>1</result> \ No newline at end of file diff --git a/tests/integration/expected/test_noVisit__Goals.getItemsCategory_day.xml b/tests/integration/expected/test_noVisit__Goals.getItemsCategory_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_noVisit__Goals.getItemsCategory_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_noVisit__Goals.getItemsName_day.xml b/tests/integration/expected/test_noVisit__Goals.getItemsName_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_noVisit__Goals.getItemsName_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_noVisit__Goals.getItemsSku_day.xml b/tests/integration/expected/test_noVisit__Goals.getItemsSku_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..c234bed59e963e268d7a9bc05348d941758c4aa9 --- /dev/null +++ b/tests/integration/expected/test_noVisit__Goals.getItemsSku_day.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result /> \ No newline at end of file diff --git a/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__API.getProcessedReport_range.xml b/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__API.getProcessedReport_range.xml index ca5197dbde291dd4cc1bccdcabc77c3f4c6217a4..3c5635cb3ffe8600efef4d003acf3035502a8aad 100644 --- a/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__API.getProcessedReport_range.xml +++ b/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__API.getProcessedReport_range.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8" ?> <result> <website>Piwik test</website> - <prettyDate>29 Apr 11 - 5 May 11</prettyDate> + <prettyDate>7 May 11 - 13 May 11</prettyDate> <metadata> <category>Visitors</category> <name>Country</name> diff --git a/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getLastVisitsDetails_range.xml b/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getLastVisitsDetails_range.xml index 5042086bf2fc6e5921f7fbe2bf487a22482a60d2..96e14ebee67a05253fa5f6911b21d33e4341760d 100644 --- a/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getLastVisitsDetails_range.xml +++ b/tests/integration/expected/test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getLastVisitsDetails_range.xml @@ -7,6 +7,7 @@ <visitorType>returning</visitorType> <visitConverted>0</visitConverted> + <visitEcommerceStatus>none</visitEcommerceStatus> <actions>1</actions> <actionDetails> <row> @@ -40,6 +41,7 @@ <visitCount>1</visitCount> <daysSinceLastVisit>0</daysSinceLastVisit> <daysSinceFirstVisit>0</daysSinceFirstVisit> + <daysSinceLastEcommerceOrder>0</daysSinceLastEcommerceOrder> <country>France</country> <countryFlag>plugins/UserCountry/flags/fr.png</countryFlag> <continent>Europe</continent> @@ -79,6 +81,8 @@ + <ecommerce> + </ecommerce> </row> <row> <idSite>1</idSite> @@ -87,6 +91,7 @@ <visitorType>new</visitorType> <visitConverted>1</visitConverted> + <visitEcommerceStatus>none</visitEcommerceStatus> <actions>1</actions> <actionDetails> <row> @@ -120,6 +125,7 @@ <visitCount>1</visitCount> <daysSinceLastVisit>0</daysSinceLastVisit> <daysSinceFirstVisit>0</daysSinceFirstVisit> + <daysSinceLastEcommerceOrder>0</daysSinceLastEcommerceOrder> <country>France</country> <countryFlag>plugins/UserCountry/flags/fr.png</countryFlag> <continent>Europe</continent> @@ -159,6 +165,8 @@ + <ecommerce> + </ecommerce> </row> <row> <idSite>1</idSite> @@ -167,6 +175,7 @@ <visitorType>new</visitorType> <visitConverted>1</visitConverted> + <visitEcommerceStatus>none</visitEcommerceStatus> <actions>3</actions> <actionDetails> <row> @@ -220,6 +229,7 @@ <visitCount>1</visitCount> <daysSinceLastVisit>0</daysSinceLastVisit> <daysSinceFirstVisit>0</daysSinceFirstVisit> + <daysSinceLastEcommerceOrder>0</daysSinceLastEcommerceOrder> <country>France</country> <countryFlag>plugins/UserCountry/flags/fr.png</countryFlag> <continent>Europe</continent> @@ -259,5 +269,7 @@ + <ecommerce> + </ecommerce> </row> </result> \ No newline at end of file diff --git a/tests/javascript/index.php b/tests/javascript/index.php index 26346747832f924c385b15b74e8d20a4d0de9080..8a60b10ae7932f094b7782cdc67084e782c3a263 100644 --- a/tests/javascript/index.php +++ b/tests/javascript/index.php @@ -721,7 +721,7 @@ if ($sqlite) { }); test("tracking", function() { - expect(52); + expect(59); /* * Prevent Opera and HtmlUnit from performing the default action (i.e., load the href URL) @@ -887,6 +887,16 @@ if ($sqlite) { tracker3.setCookieNamePrefix("PREFIX"); ok( typeof tracker3.getCustomVariable(1) == "undefined", "getCustomVariable(cvarDeleted) from cookie === undefined" ); + //Ecommerce tests + tracker3.addEcommerceItem("SKU PRODUCT", "PRODUCT NAME", "PRODUCT CATEGORY", 11.1111, 2); + tracker3.addEcommerceItem("SKU PRODUCT", "random", "random PRODUCT CATEGORY", 11.1111, 2); + tracker3.addEcommerceItem("SKU ONLY SKU", "", "", "", ""); + tracker3.addEcommerceItem("SKU ONLY NAME", "PRODUCT NAME 2", "", ""); + tracker3.addEcommerceItem("SKU NO PRICE NO QUANTITY", "PRODUCT NAME 3", "CATEGORY", "", "" ); + tracker3.addEcommerceItem("SKU ONLY" ); + tracker3.trackEcommerceCartUpdate( 555.55 ); + tracker3.trackEcommerceOrder( "ORDER ID YES", 666.66, 333, 222, 111, 1 ); + // do not track tracker3.setDoNotTrack(false); tracker3.trackPageView("DoTrack"); @@ -900,8 +910,7 @@ if ($sqlite) { xhr.open("GET", "piwik.php?requests=" + getToken(), false); xhr.send(null); results = xhr.responseText; - - equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "17", "count tracking events" ); + equal( (/<span\>([0-9]+)\<\/span\>/.exec(results))[1], "19", "count tracking events" ); // tracking requests ok( /PiwikTest/.test( results ), "trackPageView(), setDocumentTitle()" ); @@ -930,6 +939,25 @@ if ($sqlite) { ok( /DoTrack/.test( results ), "setDoNotTrack(false)" ); ok( ! /DoNotTrack/.test( results ), "setDoNotTrack(true)" ); + // Test Custom vars + ok( /_cvar=/, "test custom vars are set"); + + // Test campaign parameters set + ok( /&_rcn=YEAH&_rck=RIGHT!/.test( results), "Test campaign parameters found"); + ok( /&_ref=http%3A%2F%2Freferrer.example.com%2Fpage%2Fsub%3Fquery%3Dtest%26test2%3Dtest3/.test( results), "Test cookie Ref URL found "); + + // Ecommerce order + ok( /idgoal=0&ec_id=ORDER%20ID%20YES&revenue=666.66&ec_st=333&ec_tx=222&ec_sh=111&ec_dt=1&ec_items=%5B%5B%22SKU%20PRODUCT%22%2C%22random%22%2C%22random%20PRODUCT%20CATEGORY%22%2C11.1111%2C2%5D%2C%5B%22SKU%20ONLY%20SKU%22%2C%22%22%2C%22%22%2C0%2C1%5D%2C%5B%22SKU%20ONLY%20NAME%22%2C%22PRODUCT%20NAME%202%22%2C%22%22%2C0%2C1%5D%2C%5B%22SKU%20NO%20PRICE%20NO%20QUANTITY%22%2C%22PRODUCT%20NAME%203%22%2C%22CATEGORY%22%2C0%2C1%5D%2C%5B%22SKU%20ONLY%22%2C%22%22%2C%22%22%2C0%2C1%5D%5D/.test( results ), "logEcommerceOrder() with items" ); + + // Not set for the first ecommerce order + ok( ! /idgoal=0&ec_id=ORDER%20ID.*_ects=1/.test(results), "Ecommerce last timestamp set"); + + // Ecommerce last timestamp set properly for subsequent page view + ok( /DoTrack.*_ects=1/.test(results), "Ecommerce last timestamp set"); + + // Cart update + ok( /idgoal=0&revenue=555.55&ec_items=%5B%5B%22SKU%20PRODUCT%22%2C%22random%22%2C%22random%20PRODUCT%20CATEGORY%22%2C11.1111%2C2%5D%2C%5B%22SKU%20ONLY%20SKU%22%2C%22%22%2C%22%22%2C0%2C1%5D%2C%5B%22SKU%20ONLY%20NAME%22%2C%22PRODUCT%20NAME%202%22%2C%22%22%2C0%2C1%5D%2C%5B%22SKU%20NO%20PRICE%20NO%20QUANTITY%22%2C%22PRODUCT%20NAME%203%22%2C%22CATEGORY%22%2C0%2C1%5D%2C%5B%22SKU%20ONLY%22%2C%22%22%2C%22%22%2C0%2C1%5D%5D/.test( results ), "logEcommerceCartUpdate() with items" ); + // parameters inserted by plugin hooks ok( /testlog/.test( results ), "plugin hook log" ); ok( /testlink/.test( results ), "plugin hook link" ); diff --git a/tests/javascript/piwik.php b/tests/javascript/piwik.php index 3675e3ce45fae801abb4e8ec90edcb5eab95f091..22aea83d57353ec5e907d570eefa97ec0f6331de 100644 --- a/tests/javascript/piwik.php +++ b/tests/javascript/piwik.php @@ -63,7 +63,7 @@ if (isset($_GET['requests'])) { if($_SERVER['REQUEST_METHOD'] == 'POST') { $uri .= '?' . file_get_contents('php://input'); } - $uri = htmlspecialchars($uri); +// $uri = htmlspecialchars($uri); $referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; $ua = $_SERVER['HTTP_USER_AGENT'];