From 371af63e77c78cf302ced9926e87f427b8d2d7d8 Mon Sep 17 00:00:00 2001 From: mattpiwik <matthieu.aubry@gmail.com> Date: Sun, 15 May 2011 22:14:48 +0000 Subject: [PATCH] Refs #898 Work in progress (but should leave trunk stable and not break anything) * Now tracking Ecommerce Items (sku,name,category,qty,price) * zero, 1 or many items can be in a Ecommerce Cart (total), or an Ecommerce order (orderid, grandtotal, subtotal, tax, shipping, discount) * A Cart left at the end of a visit becomes an Abandoned cart. New reports separate orders from abandoned carts. * JS API and PHP API have 3 new functions (add items, track cart update, track ecommerce order) * JS stores timestamp last ecommerce transaction in id cookie so we can count repeat buyers * Goals.get API now returns stats for the two goals: ecommerceOrder and ecommerceAbandonedCart * new API functions to request Items (product) reports: getItemsSku, getItemsName, getItemsCategory. See doc: 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) * showing ecommerce orders/abandoned carts in the Live! API output * new segments: visitEcommerceStatus and daysSinceLastEcommerceOrder * these new attributes also appears in Live! API output * Fixes #1975 as side effect of cleaning up all this code this bug should now be fixed (bug was to use nb of conversions as dividend, rather than number of converted visits) * adding full integration test testing all possible use cases regarding Ecommerce carts/orders/items etc. * also integration testing the changes to piwik.js git-svn-id: http://dev.piwik.org/svn/trunk@4691 59fd770c-687e-43c8-a1e3-f5a4ff64c105 --- core/API/DocumentationGenerator.php | 3 +- core/API/Proxy.php | 3 +- core/Archive.php | 28 +- core/ArchiveProcessing/Day.php | 123 ++++- .../Filter/AddColumnsProcessedMetricsGoal.php | 7 +- core/DataTable/Filter/ReplaceColumnNames.php | 11 +- core/Db/Schema/Myisam.php | 35 +- core/Piwik.php | 7 +- core/Tracker/Action.php | 186 +++++-- core/Tracker/GoalManager.php | 503 +++++++++++++++++- core/Tracker/Visit.php | 119 +++-- core/Updates/1.5-b1.php | 79 +++ core/Version.php | 2 +- js/piwik.js | 166 +++++- lang/en.php | 6 + libs/PiwikTracker/PiwikTracker.php | 146 +++++ piwik.js | 21 +- plugins/API/API.php | 45 ++ plugins/Actions/Actions.php | 34 +- plugins/CustomVariables/CustomVariables.php | 4 +- plugins/Goals/API.php | 86 ++- plugins/Goals/Controller.php | 54 +- plugins/Goals/Goals.php | 244 +++++++-- plugins/Live/API.php | 75 ++- plugins/Live/Visitor.php | 11 + plugins/PDFReports/API.php | 1 - plugins/Referers/Referers.php | 12 +- plugins/UserCountry/UserCountry.php | 19 +- plugins/VisitFrequency/API.php | 15 + tests/integration/Integration.php | 11 +- tests/integration/Main.test.php | 118 +++- ...rTwoVisits__Goals.getItemsCategory_day.xml | 2 + ...sitorTwoVisits__Goals.getItemsName_day.xml | 2 + ...isitorTwoVisits__Goals.getItemsSku_day.xml | 2 + ...kieSupport__Goals.getItemsCategory_day.xml | 2 + ...hCookieSupport__Goals.getItemsName_day.xml | 2 + ...thCookieSupport__Goals.getItemsSku_day.xml | 2 + ...ignTracking__Referers.getCampaigns_day.xml | 8 +- ...fferentDays__Actions.getPageTitles_day.xml | 4 +- ...erentDays__Actions.getPageTitles_month.xml | 4 +- ...ferentDays__Actions.getPageTitles_week.xml | 4 +- ...ferentDays__Actions.getPageTitles_year.xml | 4 +- ...differentDays__Actions.getPageUrls_day.xml | 4 +- ...fferentDays__Actions.getPageUrls_month.xml | 4 +- ...ifferentDays__Actions.getPageUrls_week.xml | 4 +- ...ifferentDays__Actions.getPageUrls_year.xml | 4 +- ..._apiGetReportMetadata__API.getMetadata.xml | 36 -- ...rtMetadata__API.getProcessedReport_day.xml | 2 +- ...eportMetadata__API.getSegmentsMetadata.xml | 13 + ...donedCarts__Goals.getItemsCategory_day.xml | 19 + ...onedCarts__Goals.getItemsCategory_week.xml | 19 + ...AbandonedCarts__Goals.getItemsName_day.xml | 19 + ...bandonedCarts__Goals.getItemsName_week.xml | 19 + ..._AbandonedCarts__Goals.getItemsSku_day.xml | 19 + ...AbandonedCarts__Goals.getItemsSku_week.xml | 19 + ...Items_GoalAbandonedCart__Goals.get_day.xml | 8 + ...tems_GoalAbandonedCart__Goals.get_week.xml | 8 + ...ithItems_GoalMatchTitle__Goals.get_day.xml | 7 + ...thItems_GoalMatchTitle__Goals.get_week.xml | 7 + ...rderWithItems_GoalOrder__Goals.get_day.xml | 12 + ...derWithItems_GoalOrder__Goals.get_week.xml | 12 + ...erWithItems_GoalOverall__Goals.get_day.xml | 7 + ...rWithItems_GoalOverall__Goals.get_week.xml | 7 + ...rdered__Live.getLastVisitsDetails_day.xml} | 145 +++-- ...ntAbandonedCart__VisitsSummary.get_day.xml | 13 + ...mentNoEcommerce__VisitsSummary.get_day.xml | 13 + ...rderedSomething__VisitsSummary.get_day.xml | 13 + ...rWithItems__API.getProcessedReport_day.xml | 84 +++ ...derWithItems__Goals.getConversions_day.xml | 2 + ...rWithItems__Goals.getItemsCategory_day.xml | 11 + ...WithItems__Goals.getItemsCategory_week.xml | 19 + ...OrderWithItems__Goals.getItemsName_day.xml | 19 + ...rderWithItems__Goals.getItemsName_week.xml | 27 + ...eOrderWithItems__Goals.getItemsSku_day.xml | 19 + ...OrderWithItems__Goals.getItemsSku_week.xml | 27 + ...ecommerceOrderWithItems__Goals.get_day.xml | 7 + ...commerceOrderWithItems__Goals.get_week.xml | 7 + ...thItems__Live.getLastVisitsDetails_day.xml | 273 ++++++++++ ...ithItems__UserCountry.getContinent_day.xml | 38 ++ ...rWithItems__UserCountry.getCountry_day.xml | 41 ++ ...untry.getNumberOfDistinctCountries_day.xml | 2 + ...st_noVisit__Goals.getItemsCategory_day.xml | 2 + .../test_noVisit__Goals.getItemsName_day.xml | 2 + .../test_noVisit__Goals.getItemsSku_day.xml | 2 + ...ormalAPI__API.getProcessedReport_range.xml | 2 +- ...alAPI__Live.getLastVisitsDetails_range.xml | 12 + tests/javascript/index.php | 34 +- tests/javascript/piwik.php | 2 +- 88 files changed, 2852 insertions(+), 423 deletions(-) create mode 100644 core/Updates/1.5-b1.php create mode 100644 tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsCategory_day.xml create mode 100644 tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsName_day.xml create mode 100644 tests/integration/expected/test_OneVisitorTwoVisits__Goals.getItemsSku_day.xml create mode 100644 tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsCategory_day.xml create mode 100644 tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsName_day.xml create mode 100644 tests/integration/expected/test_OneVisitorTwoVisits_withCookieSupport__Goals.getItemsSku_day.xml delete mode 100644 tests/integration/expected/test_apiGetReportMetadata__API.getMetadata.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsCategory_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsName_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_AbandonedCarts__Goals.getItemsSku_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_GoalAbandonedCart__Goals.get_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_GoalMatchTitle__Goals.get_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_GoalOrder__Goals.get_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_GoalOverall__Goals.get_week.xml rename tests/integration/expected/{test_periodIsRange_dateIsLastN_MetadataAndNormalAPI__Live.getLastVisitsForVisitor.xml => test_ecommerceOrderWithItems_LiveEcommerceStatusOrdered__Live.getLastVisitsDetails_day.xml} (55%) create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_SegmentAbandonedCart__VisitsSummary.get_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_SegmentNoEcommerce__VisitsSummary.get_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems_SegmentOrderedSomething__VisitsSummary.get_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__API.getProcessedReport_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.getConversions_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsCategory_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsName_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsName_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.getItemsSku_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.get_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Goals.get_week.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__Live.getLastVisitsDetails_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getContinent_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getCountry_day.xml create mode 100644 tests/integration/expected/test_ecommerceOrderWithItems__UserCountry.getNumberOfDistinctCountries_day.xml create mode 100644 tests/integration/expected/test_noVisit__Goals.getItemsCategory_day.xml create mode 100644 tests/integration/expected/test_noVisit__Goals.getItemsName_day.xml create mode 100644 tests/integration/expected/test_noVisit__Goals.getItemsSku_day.xml diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php index 074783bec1..6b087b8f03 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 61e3a2da16..1a283e221e 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 65c26ae03a..4a70e43eee 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 cb00e5cc4f..37b4b187ee 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 eb4359437e..50a1e665b2 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 2d22de7046..810ba5c322 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 2f19b54d52..007d174e40 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 7af1de6abd..5bb6ea89e4 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 3efe824275..49f635bf77 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 d32ae8a5b6..ec91de3d15 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 089477439d..1162ea1585 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 0000000000..3fcd4a6937 --- /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 d030725997..f9cabbd42d 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 510e8f312e..0e1c0e1bea 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 2d56104b31..53e3642977 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 b4ad4ba5f8..a7f04e1254 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 5872246f71..7f0cbabdc6 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 b46f1328a0..93b46a2e25 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 c93aff5ec6..789a96731d 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 80c0e07501..e9da3d5390 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 65d902813e..2b3fadfbf3 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 96ed86b34d..728caec14f 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 4b1a15d3d3..049117dc86 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 3c74dd3da1..3460c83871 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 1d03159201..77b97c0c81 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 38d8a8a20b..22b1bde7a7 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 092e774a2b..4d5fa40697 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 fc72df6e6d..8fcb565da6 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 8cb246bd32..074377974d 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 2edabc6d8c..954a5d7097 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 322b732d5e..25fda05522 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 0000000000..c234bed59e --- /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 0000000000..c234bed59e --- /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 0000000000..c234bed59e --- /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 0000000000..c234bed59e --- /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 0000000000..c234bed59e --- /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 0000000000..c234bed59e --- /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 9b6c31b055..69520dea3d 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 12de83797f..e91916f991 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 e0dd626f42..dcfde2bd99 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 5ad111fece..11c2526451 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 5776b676b7..5ae46a54f3 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 fea2843acc..92cc681d9d 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 6b29877f2b..b9d1acb081 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 9225c807cc..a6916d1058 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 0ce97c43b0..19b419e61f 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 1c8ffd506b..0000000000 --- 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 0ac0489070..a9b9650936 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 94659d9ff4..9bcc275e38 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 0000000000..53a82090ef --- /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 0000000000..263c329f3e --- /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 0000000000..c3b0b3a358 --- /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 0000000000..1d603853ae --- /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 0000000000..2d73a799ff --- /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 0000000000..59fd6ae2ef --- /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 0000000000..693e7dbc32 --- /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 0000000000..5c8effaf28 --- /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 0000000000..fc62e5e6a5 --- /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 0000000000..c29e706e50 --- /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 0000000000..857aa10333 --- /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 0000000000..af971d8048 --- /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 0000000000..5867f1dd24 --- /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 0000000000..3b8f443470 --- /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 9a016901b2..d2520843fa 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 0000000000..e8e1fa23a4 --- /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 0000000000..a4933d174c --- /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 0000000000..95b8e6c667 --- /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 0000000000..959c03d202 --- /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 0000000000..15ef03fb49 --- /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 0000000000..0112cf98d4 --- /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 0000000000..84a8f9e899 --- /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 0000000000..d72196172a --- /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 0000000000..4199ef3337 --- /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 0000000000..43bc239c71 --- /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 0000000000..6263163fd8 --- /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 0000000000..5867f1dd24 --- /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 0000000000..3b8f443470 --- /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 0000000000..7fddc2974f --- /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 0000000000..f96445a3b9 --- /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 0000000000..2509d485b5 --- /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 0000000000..606fbb5241 --- /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 0000000000..c234bed59e --- /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 0000000000..c234bed59e --- /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 0000000000..c234bed59e --- /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 ca5197dbde..3c5635cb3f 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 5042086bf2..96e14ebee6 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 2634674783..8a60b10ae7 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 3675e3ce45..22aea83d57 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']; -- GitLab