From ed9cd9eb2bd61b2db833092f31606fe68d7ae396 Mon Sep 17 00:00:00 2001 From: mattab Date: Sun, 20 Oct 2013 16:54:21 +1300 Subject: [PATCH] Some refactoring and preparations for custom events ref #472 PHP Tracker and Tests fixtures Schema updates --- .gitignore | 24 +- core/Db/Schema/Myisam.php | 3 + core/RankingQuery.php | 2 +- core/Tracker/Action.php | 377 +------- core/Tracker/GoalManager.php | 83 +- core/Tracker/PageUrl.php | 330 +++++++ core/Tracker/Referrer.php | 2 +- core/Tracker/Visit.php | 2 +- core/Updates/2.0-b2.php | 39 + core/Version.php | 2 +- libs/PiwikTracker/PiwikTracker.php | 60 +- plugins/Actions/API.php | 13 +- plugins/Actions/Actions.php | 6 +- plugins/Actions/Archiver.php | 126 +-- plugins/Actions/ArchivingHelper.php | 111 ++- plugins/Live/API.php | 4 +- plugins/Overlay/API.php | 4 +- plugins/Overlay/Controller.php | 5 +- plugins/Transitions/API.php | 21 +- tests/PHPUnit/BaseFixture.php | 1 + tests/PHPUnit/Core/Tracker/ActionTest.php | 43 +- .../PHPUnit/Fixtures/ManyVisitsWithGeoIP.php | 2 +- ...SomeVisitsManyPageviewsWithTransitions.php | 1 - .../TwoSitesTwoVisitorsDifferentDays.php | 1 - .../Fixtures/TwoVisitsWithCustomEvents.php | 155 ++++ .../PHPUnit/Integration/CustomEventsTest.php | 85 ++ .../Integration/UrlNormalizationTest.php | 2 +- ...Urls_lastN__API.getProcessedReport_day.xml | 86 ++ ..._CustomEvents__Actions.getPageUrls_day.xml | 37 + ...ustomEvents__Actions.getPageUrls_month.xml | 37 + .../test_CustomEvents__Actions.get_day.xml | 12 + .../test_CustomEvents__Actions.get_month.xml | 12 + ...mEvents__Live.getLastVisitsDetails_day.xml | 805 ++++++++++++++++++ ...vents__Live.getLastVisitsDetails_month.xml | 805 ++++++++++++++++++ tests/PHPUnit/Plugins/ActionsTest.php | 32 +- 35 files changed, 2745 insertions(+), 585 deletions(-) create mode 100644 core/Tracker/PageUrl.php create mode 100644 core/Updates/2.0-b2.php create mode 100644 tests/PHPUnit/Fixtures/TwoVisitsWithCustomEvents.php create mode 100644 tests/PHPUnit/Integration/CustomEventsTest.php create mode 100644 tests/PHPUnit/Integration/expected/test_CustomEvents_Actions.getPageUrls_lastN__API.getProcessedReport_day.xml create mode 100644 tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.getPageUrls_day.xml create mode 100644 tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.getPageUrls_month.xml create mode 100644 tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.get_day.xml create mode 100644 tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.get_month.xml create mode 100644 tests/PHPUnit/Integration/expected/test_CustomEvents__Live.getLastVisitsDetails_day.xml create mode 100644 tests/PHPUnit/Integration/expected/test_CustomEvents__Live.getLastVisitsDetails_month.xml diff --git a/.gitignore b/.gitignore index b0a1b780645..05529382ee1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,35 @@ bootstrap.php build +composer.phar +config/config.ini.php crossdomain.xml documentation +docs/ favicon.ico +js/yui* logs -plugins/*.zip +misc/*.dat misc/user/logo-header.png misc/user/logo.png misc/user/logo.svg +php_errors.log piwik-min.js +plugins/*.zip robots.txt tmp tmp/* +vendor/ + .cache +.DS_Store .externalToolBuilders .idea/* .metadata -.settings .project -*.tmp -*.db +.settings *.buildpath -config/config.ini.php -.DS_Store -js/yui* -misc/*.dat +*.tmp +tests/javascript/enable_sqlite tests/javascript/enable_sqlite tests/javascript/unittest.dbf tests/lib/geoip-files/*.dat* @@ -59,9 +64,6 @@ tests/lib/xhprof-0.9.2/extension/ltmain.sh tests/lib/xhprof-0.9.2/extension/missing tests/lib/xhprof-0.9.2/extension/mkinstalldirs tests/lib/xhprof-0.9.2/extension/run-tests.php -docs/ -composer.phar -vendor/ /.htaccess config/.htaccess core/.htaccess diff --git a/core/Db/Schema/Myisam.php b/core/Db/Schema/Myisam.php index d7de28b87a2..5313fda2444 100644 --- a/core/Db/Schema/Myisam.php +++ b/core/Db/Schema/Myisam.php @@ -165,6 +165,7 @@ public function getTablesCreateSql() visit_entry_idaction_name INTEGER(11) UNSIGNED NOT NULL, visit_total_actions SMALLINT(5) UNSIGNED NOT NULL, visit_total_searches SMALLINT(5) UNSIGNED NOT NULL, + visit_total_events 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, @@ -292,6 +293,8 @@ public function getTablesCreateSql() idaction_url_ref INTEGER(10) UNSIGNED NULL DEFAULT 0, idaction_name INTEGER(10) UNSIGNED, idaction_name_ref INTEGER(10) UNSIGNED NOT NULL, + idaction_event_category INTEGER(10) UNSIGNED, + idaction_event_action INTEGER(10) UNSIGNED, time_spent_ref_action INTEGER(10) UNSIGNED NOT NULL, custom_var_k1 VARCHAR(200) DEFAULT NULL, custom_var_v1 VARCHAR(200) DEFAULT NULL, diff --git a/core/RankingQuery.php b/core/RankingQuery.php index e72d76f1930..52802336aea 100644 --- a/core/RankingQuery.php +++ b/core/RankingQuery.php @@ -177,7 +177,7 @@ public function setColumnToMarkExcludedRows($column) /** * This method can be used to get multiple groups in one go. For example, one might query * the top following pages, outlinks and downloads in one go by using log_action.type as - * the partition column and [TYPE_ACTION_URL, TYPE_OUTLINK, TYPE_DOWNLOAD] as the possible + * the partition column and [TYPE_PAGE_URL, TYPE_OUTLINK, TYPE_DOWNLOAD] as the possible * values. * When this method has been used, generate() returns as array that contains one array * per group of data. diff --git a/core/Tracker/Action.php b/core/Tracker/Action.php index 6a47669b766..842b185ab2d 100644 --- a/core/Tracker/Action.php +++ b/core/Tracker/Action.php @@ -47,80 +47,18 @@ class Action implements ActionInterface /** * Encoding of HTML page being viewed. See reencodeParameters for more info. - * * @var string */ private $pageEncoding = false; - static private $queryParametersToExclude = array('gclid', 'phpsessid', 'jsessionid', 'sessionid', 'aspsessionid', 'fb_xd_fragment', 'fb_comment_id', - 'doing_wp_cron'); - /* Custom Variable names & slots used for Site Search metadata (category, results count) */ const CVAR_KEY_SEARCH_CATEGORY = '_pk_scat'; const CVAR_KEY_SEARCH_COUNT = '_pk_scount'; const CVAR_INDEX_SEARCH_CATEGORY = '4'; const CVAR_INDEX_SEARCH_COUNT = '5'; - /* Custom Variables names & slots plus Tracking API Parameters for performance analytics */ const DB_COLUMN_TIME_GENERATION = 'custom_float'; - /** - * Map URL prefixes to integers. - * @see self::normalizeUrl(), self::reconstructNormalizedUrl() - */ - static public $urlPrefixMap = array( - 'http://www.' => 1, - 'http://' => 0, - 'https://www.' => 3, - 'https://' => 2 - ); - - /** - * Extract the prefix from a URL. - * Return the prefix ID and the rest. - * - * @param string $url - * @return array - */ - static public function normalizeUrl($url) - { - foreach (self::$urlPrefixMap as $prefix => $id) { - if (strtolower(substr($url, 0, strlen($prefix))) == $prefix) { - return array( - 'url' => substr($url, strlen($prefix)), - 'prefixId' => $id - ); - } - } - return array('url' => $url, 'prefixId' => null); - } - - /** - * Build the full URL from the prefix ID and the rest. - * - * @param string $url - * @param integer $prefixId - * @return string - */ - static public function reconstructNormalizedUrl($url, $prefixId) - { - $map = array_flip(self::$urlPrefixMap); - if ($prefixId !== null && isset($map[$prefixId])) { - $fullUrl = $map[$prefixId] . $url; - } else { - $fullUrl = $url; - } - - // Clean up host & hash tags, for URLs - $parsedUrl = @parse_url($fullUrl); - $parsedUrl = self::cleanupHostAndHashTag($parsedUrl); - $url = UrlHelper::getParseUrlReverse($parsedUrl); - if (!empty($url)) { - return $url; - } - - return $fullUrl; - } public function __construct(Request $request) { @@ -154,8 +92,8 @@ public function getActionNameType() // we can add here action types for names of other actions than page views (like downloads, outlinks) switch ($this->getActionType()) { - case ActionInterface::TYPE_ACTION_URL: - $actionNameType = ActionInterface::TYPE_ACTION_NAME; + case ActionInterface::TYPE_PAGE_URL: + $actionNameType = ActionInterface::TYPE_PAGE_TITLE; break; case ActionInterface::TYPE_SITE_SEARCH: @@ -189,7 +127,7 @@ public function getIdActionName() protected function setActionName($name) { - $name = self::cleanupString($name); + $name = PageUrl::cleanupString($name); $this->actionName = $name; } @@ -203,172 +141,6 @@ protected function setActionUrl($url) $this->actionUrl = $url; } - /** - * Converts Matrix URL format - * from http://example.org/thing;paramA=1;paramB=6542 - * to http://example.org/thing?paramA=1¶mB=6542 - * - * @param string $originalUrl - * @return string - */ - static public function convertMatrixUrl($originalUrl) - { - $posFirstSemiColon = strpos($originalUrl, ";"); - if ($posFirstSemiColon === false) { - return $originalUrl; - } - $posQuestionMark = strpos($originalUrl, "?"); - $replace = ($posQuestionMark === false); - if ($posQuestionMark > $posFirstSemiColon) { - $originalUrl = substr_replace($originalUrl, ";", $posQuestionMark, 1); - $replace = true; - } - if ($replace) { - $originalUrl = substr_replace($originalUrl, "?", strpos($originalUrl, ";"), 1); - $originalUrl = str_replace(";", "&", $originalUrl); - } - return $originalUrl; - } - - static public function cleanupUrl($url) - { - $url = Common::unsanitizeInputValue($url); - $url = self::cleanupString($url); - $url = self::convertMatrixUrl($url); - return $url; - } - - /** - * Will cleanup the hostname (some browser do not strolower the hostname), - * and deal ith the hash tag on incoming URLs based on website setting. - * - * @param $parsedUrl - * @param $idSite int|bool The site ID of the current visit. This parameter is - * only used by the tracker to see if we should remove - * the URL fragment for this site. - * @return array - */ - static public function cleanupHostAndHashTag($parsedUrl, $idSite = false) - { - if (empty($parsedUrl)) { - return $parsedUrl; - } - if (!empty($parsedUrl['host'])) { - $parsedUrl['host'] = mb_strtolower($parsedUrl['host'], 'UTF-8'); - } - - if (!empty($parsedUrl['fragment'])) { - $parsedUrl['fragment'] = self::processUrlFragment($parsedUrl['fragment'], $idSite); - } - - return $parsedUrl; - } - - /** - * Cleans and/or removes the URL fragment of a URL. - * - * @param $urlFragment string The URL fragment to process. - * @param $idSite int|bool If not false, this function will check if URL fragments - * should be removed for the site w/ this ID and if so, - * the returned processed fragment will be empty. - * - * @return string The processed URL fragment. - */ - public static function processUrlFragment($urlFragment, $idSite = false) - { - // if we should discard the url fragment for this site, return an empty string as - // the processed url fragment - if ($idSite !== false - && self::shouldRemoveURLFragmentFor($idSite) - ) { - return ''; - } else { - // Remove trailing Hash tag in ?query#hash# - if (substr($urlFragment, -1) == '#') { - $urlFragment = substr($urlFragment, 0, strlen($urlFragment) - 1); - } - return $urlFragment; - } - } - - /** - * Returns true if URL fragments should be removed for a specific site, - * false if otherwise. - * - * This function uses the Tracker cache and not the MySQL database. - * - * @param $idSite int The ID of the site to check for. - * @return bool - */ - public static function shouldRemoveURLFragmentFor($idSite) - { - $websiteAttributes = Cache::getCacheWebsiteAttributes($idSite); - return !$websiteAttributes['keep_url_fragment']; - } - - /** - * Given the Input URL, will exclude all query parameters set for this site - * Note: Site Search parameters are excluded in detectSiteSearch() - * @static - * @param $originalUrl - * @param $idSite - * @return bool|string - */ - static public function excludeQueryParametersFromUrl($originalUrl, $idSite) - { - $originalUrl = self::cleanupUrl($originalUrl); - - $parsedUrl = @parse_url($originalUrl); - $parsedUrl = self::cleanupHostAndHashTag($parsedUrl, $idSite); - $parametersToExclude = self::getQueryParametersToExclude($idSite); - - if (empty($parsedUrl['query'])) { - if (empty($parsedUrl['fragment'])) { - return UrlHelper::getParseUrlReverse($parsedUrl); - } - // Exclude from the hash tag as well - $queryParameters = UrlHelper::getArrayFromQueryString($parsedUrl['fragment']); - $parsedUrl['fragment'] = UrlHelper::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude); - $url = UrlHelper::getParseUrlReverse($parsedUrl); - return $url; - } - $queryParameters = UrlHelper::getArrayFromQueryString($parsedUrl['query']); - $parsedUrl['query'] = UrlHelper::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude); - $url = UrlHelper::getParseUrlReverse($parsedUrl); - return $url; - } - - /** - * Returns the array of parameters names that must be excluded from the Query String in all tracked URLs - * @static - * @param $idSite - * @return array - */ - public static function getQueryParametersToExclude($idSite) - { - $campaignTrackingParameters = Common::getCampaignParameters(); - - $campaignTrackingParameters = array_merge( - $campaignTrackingParameters[0], // campaign name parameters - $campaignTrackingParameters[1] // campaign keyword parameters - ); - - $website = Cache::getCacheWebsiteAttributes($idSite); - $excludedParameters = isset($website['excluded_parameters']) - ? $website['excluded_parameters'] - : array(); - - if (!empty($excludedParameters)) { - Common::printDebug('Excluding parameters "' . implode(',', $excludedParameters) . '" from URL'); - } - - $parametersToExclude = array_merge($excludedParameters, - self::$queryParametersToExclude, - $campaignTrackingParameters); - - $parametersToExclude = array_map('strtolower', $parametersToExclude); - return $parametersToExclude; - } protected function init() { @@ -377,7 +149,7 @@ protected function init() $info = $this->extractUrlAndActionNameFromRequest(); $originalUrl = $info['url']; - $info['url'] = self::excludeQueryParametersFromUrl($originalUrl, $this->request->getIdSite()); + $info['url'] = PageUrl::excludeQueryParametersFromUrl($originalUrl, $this->request->getIdSite()); if ($originalUrl != $info['url']) { Common::printDebug(' Before was "' . $originalUrl . '"'); @@ -425,9 +197,9 @@ static public function loadActionId($actionNamesAndTypes) if ($i > 0) { $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) "; } - if ($type == Tracker\Action::TYPE_ACTION_URL) { + if ($type == Tracker\Action::TYPE_PAGE_URL) { // normalize urls by stripping protocol and www - $normalizedUrls[$index] = self::normalizeUrl($name); + $normalizedUrls[$index] = PageUrl::normalizeUrl($name); $name = $normalizedUrls[$index]['url']; } $bind[] = $name; @@ -491,7 +263,7 @@ static public function loadActionId($actionNamesAndTypes) static public function getActionTypeName($type) { switch ($type) { - case self::TYPE_ACTION_URL: + case self::TYPE_PAGE_URL: return 'Page URL'; break; case self::TYPE_OUTLINK: @@ -500,7 +272,7 @@ static public function getActionTypeName($type) case self::TYPE_DOWNLOAD: return 'Download URL'; break; - case self::TYPE_ACTION_NAME: + case self::TYPE_PAGE_TITLE: return 'Page Title'; break; case self::TYPE_SITE_SEARCH: @@ -549,7 +321,7 @@ function loadIdActionNameAndUrl() // this code is a mess, but basically, getActionType() returns SITE_SEARCH, // but we do want to record the site search URL as an ACTION_URL if ($nameType == Tracker\Action::TYPE_SITE_SEARCH) { - $urlType = Tracker\Action::TYPE_ACTION_URL; + $urlType = Tracker\Action::TYPE_PAGE_URL; // By default, Site Search does not record the URL for the Search Result page, to slightly improve performance if (empty(Config::getInstance()->Tracker['action_sitesearch_record_url'])) { @@ -564,7 +336,7 @@ function loadIdActionNameAndUrl() foreach ($loadedActionIds as $loadedActionId) { list($name, $type, $actionId) = $loadedActionId; - if ($type == Tracker\Action::TYPE_ACTION_NAME + if ($type == Tracker\Action::TYPE_PAGE_TITLE || $type == Tracker\Action::TYPE_SITE_SEARCH ) { $this->idActionName = $actionId; @@ -588,8 +360,8 @@ public function record($idVisit, $visitorIdCookie, $idReferrerActionUrl, $idRefe { $this->loadIdActionNameAndUrl(); - $idActionName = in_array($this->getActionType(), array(Tracker\Action::TYPE_ACTION_NAME, - Tracker\Action::TYPE_ACTION_URL, + $idActionName = in_array($this->getActionType(), array(Tracker\Action::TYPE_PAGE_TITLE, + Tracker\Action::TYPE_PAGE_URL, Tracker\Action::TYPE_SITE_SEARCH)) ? (int)$this->getIdActionName() : null; @@ -693,16 +465,15 @@ public function getIdLinkVisitAction() */ protected function extractUrlAndActionNameFromRequest() { - $actionName = null; + $actionName = $this->request->getParam('action_name'); + $url = $this->request->getParam('url'); - // download? $downloadUrl = $this->request->getParam('download'); if (!empty($downloadUrl)) { $actionType = self::TYPE_DOWNLOAD; $url = $downloadUrl; } - // outlink? if (empty($actionType)) { $outlinkUrl = $this->request->getParam('link'); if (!empty($outlinkUrl)) { @@ -711,39 +482,10 @@ protected function extractUrlAndActionNameFromRequest() } } - // handle encoding - $actionName = $this->request->getParam('action_name'); - // defaults to page view if (empty($actionType)) { - $actionType = self::TYPE_ACTION_URL; - $url = $this->request->getParam('url'); - - // get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067) - $actionCategoryDelimiter = isset(Config::getInstance()->General['action_category_delimiter']) - ? Config::getInstance()->General['action_category_delimiter'] - : Config::getInstance()->General['action_url_category_delimiter']; - - // create an array of the categories delimited by the delimiter - $split = explode($actionCategoryDelimiter, $actionName); - - // trim every category - $split = array_map('trim', $split); - - // remove empty categories - $split = array_filter($split, 'strlen'); - - // rebuild the name from the array of cleaned categories - $actionName = implode($actionCategoryDelimiter, $split); - } - $url = self::cleanupString($url); - - if (!UrlHelper::isLookLikeUrl($url)) { - Common::printDebug("WARNING: URL looks invalid and is discarded"); - $url = ''; - } - - if ($actionType == self::TYPE_ACTION_URL) { + $actionType = self::TYPE_PAGE_URL; + $actionName = $this->cleanupActionName($actionName); // Look in tracked URL for the Site Search parameters $siteSearch = $this->detectSiteSearch($url); @@ -755,10 +497,11 @@ protected function extractUrlAndActionNameFromRequest() // Look for performance analytics parameters $this->timeGeneration = $this->request->getPageGenerationTime(); } - $actionName = self::cleanupString($actionName); + $url = PageUrl::getUrlIfLookValid($url); + $actionName = PageUrl::cleanupString((string)$actionName); return array( - 'name' => empty($actionName) ? '' : $actionName, + 'name' => $actionName, 'type' => $actionType, 'url' => $url, ); @@ -774,7 +517,7 @@ protected function detectSiteSearch($originalUrl) $actionName = $url = $categoryName = $count = false; $doTrackUrlForSiteSearch = !empty(Config::getInstance()->Tracker['action_sitesearch_record_url']); - $originalUrl = self::cleanupUrl($originalUrl); + $originalUrl = PageUrl::cleanupUrl($originalUrl); // Detect Site search from Tracking API parameters rather than URL $searchKwd = $this->request->getParam('search'); @@ -864,7 +607,7 @@ protected function detectSiteSearchFromUrl($website, $parsedUrl) $parameters[Common::mb_strtolower($k)] = $v; } // decode values if they were sent from a client using another charset - self::reencodeParameters($parameters, $this->pageEncoding); + PageUrl::reencodeParameters($parameters, $this->pageEncoding); // Detect Site Search keyword foreach ($keywordParameters as $keywordParameterRaw) { @@ -919,70 +662,28 @@ protected function detectSiteSearchFromUrl($website, $parsedUrl) return array($url, $actionName, $categoryName, $count); } - /** - * Clean up string contents (filter, truncate, ...) - * - * @param string $string Dirty string - * @return string - */ - protected static function cleanupString($string) + protected function cleanupActionName($actionName) { - $string = trim($string); - $string = str_replace(array("\n", "\r", "\0"), '', $string); + // get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067) + $actionCategoryDelimiter = isset(Config::getInstance()->General['action_category_delimiter']) + ? Config::getInstance()->General['action_category_delimiter'] + : Config::getInstance()->General['action_url_category_delimiter']; - $limit = Config::getInstance()->Tracker['page_maximum_length']; - return substr($string, 0, $limit); - } + // create an array of the categories delimited by the delimiter + $split = explode($actionCategoryDelimiter, $actionName); - /** - * Checks if query parameters are of a non-UTF-8 encoding and converts the values - * from the specified encoding to UTF-8. - * This method is used to workaround browser/webapp bugs (see #3450). When - * browsers fail to encode query parameters in UTF-8, the tracker will send the - * charset of the page viewed and we can sometimes work around invalid data - * being stored. - * - * @param array $queryParameters Name/value mapping of query parameters. - * @param bool|string $encoding of the HTML page the URL is for. Used to workaround - * browser bugs & mis-coded webapps. See #3450. - * - * @return array - */ - private static function reencodeParameters(&$queryParameters, $encoding = false) - { - // if query params are encoded w/ non-utf8 characters (due to browser bug or whatever), - // encode to UTF-8. - if ($encoding !== false - && strtolower($encoding) != 'utf-8' - && function_exists('mb_check_encoding') - ) { - $queryParameters = self::reencodeParametersArray($queryParameters, $encoding); - } - return $queryParameters; - } + // trim every category + $split = array_map('trim', $split); - private static function reencodeParametersArray($queryParameters, $encoding) - { - foreach ($queryParameters as &$value) { - if (is_array($value)) { - $value = self::reencodeParametersArray($value, $encoding); - } else { - $value = self::reencodeParameterValue($value, $encoding); - } - } - return $queryParameters; - } + // remove empty categories + $split = array_filter($split, 'strlen'); - private static function reencodeParameterValue($value, $encoding) - { - if (is_string($value)) { - $decoded = urldecode($value); - if (@mb_check_encoding($decoded, $encoding)) { - $value = urlencode(mb_convert_encoding($decoded, 'UTF-8', $encoding)); - } - } - return $value; + // rebuild the name from the array of cleaned categories + $actionName = implode($actionCategoryDelimiter, $split); + return $actionName; } + + } @@ -995,10 +696,10 @@ private static function reencodeParameterValue($value, $encoding) */ interface ActionInterface { - const TYPE_ACTION_URL = 1; + const TYPE_PAGE_URL = 1; const TYPE_OUTLINK = 2; const TYPE_DOWNLOAD = 3; - const TYPE_ACTION_NAME = 4; + const TYPE_PAGE_TITLE = 4; const TYPE_ECOMMERCE_ITEM_SKU = 5; const TYPE_ECOMMERCE_ITEM_NAME = 6; const TYPE_ECOMMERCE_ITEM_CATEGORY = 7; diff --git a/core/Tracker/GoalManager.php b/core/Tracker/GoalManager.php index 3e3fcf874e2..00975d5cef0 100644 --- a/core/Tracker/GoalManager.php +++ b/core/Tracker/GoalManager.php @@ -144,7 +144,7 @@ function detectGoalsMatchingUrl($idSite, $action) foreach ($goals as $goal) { $attribute = $goal['match_attribute']; // if the attribute to match is not the type of the current action - if (($actionType == Action::TYPE_ACTION_URL && $attribute != 'url' && $attribute != 'title') + if (($actionType == Action::TYPE_PAGE_URL && $attribute != 'url' && $attribute != 'title') || ($actionType == Action::TYPE_DOWNLOAD && $attribute != 'file') || ($actionType == Action::TYPE_OUTLINK && $attribute != 'external_website') || ($attribute == 'manually') @@ -213,7 +213,7 @@ function detectGoalId($idSite) $goal = $goals[$this->idGoal]; $url = $this->request->getParam('url'); - $goal['url'] = Action::excludeQueryParametersFromUrl($url, $idSite); + $goal['url'] = PageUrl::excludeQueryParametersFromUrl($url, $idSite); $goal['revenue'] = $this->getRevenue($this->request->getGoalRevenue($goal['revenue'])); $this->convertedGoals[] = $goal; return true; @@ -356,13 +356,6 @@ protected function getRevenue($revenue) */ 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) { Common::printDebug("There is an existing cart for this visit"); } @@ -398,10 +391,17 @@ protected function recordEcommerceGoal($goal, $visitorInformation) } $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($this->isThereExistingCartInVisit) { + $updateWhere = array( + 'idvisit' => $visitorInformation['idvisit'], + 'idgoal' => self::IDGOAL_CART, + 'buster' => 0, + ); + $recorded = $this->updateExistingConversion($goal, $updateWhere); + } else { + $recorded = $this->insertNewConversion($goal); + } + if ($recorded) { $this->recordEcommerceItems($goal, $items); } @@ -409,8 +409,9 @@ protected function recordEcommerceGoal($goal, $visitorInformation) /** * This hook is called after recording an ecommerce goal. You can use it for instance to sync the recorded goal * with third party systems. `$goal` contains all available information like `items` and `revenue`. + * `$visitor` contains the current known visit information. */ - Piwik::postEvent('Tracker.recordEcommerceGoal', array($goal)); + Piwik::postEvent('Tracker.recordEcommerceGoal', array($goal, $visitorInformation)); } /** @@ -769,7 +770,7 @@ protected function recordStandardGoals($goal, $action, $visitorInformation) ? '0' : $visitorInformation['visit_last_action_time']; - $this->recordGoal($newGoal); + $this->insertNewConversion($newGoal); /** * This hook is called after recording a standard goal. You can use it for instance to sync the recorded @@ -783,11 +784,9 @@ protected function recordStandardGoals($goal, $action, $visitorInformation) * 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 * @return bool */ - protected function recordGoal($newGoal, $mustUpdateNotInsert = false, $updateWhere = array()) + protected function insertNewConversion($newGoal) { $newGoalDebug = $newGoal; $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']); @@ -795,31 +794,13 @@ protected function recordGoal($newGoal, $mustUpdateNotInsert = false, $updateWhe $fields = implode(", ", array_keys($newGoal)); $bindFields = Common::getSqlStringFieldsArray($newGoal); + $sql = 'INSERT IGNORE INTO ' . Common::prefixTable('log_conversion') . " + ($fields) VALUES ($bindFields) "; + $bind = array_values($newGoal); + $result = Tracker::getDatabase()->query($sql, $bind); - 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 ' . Common::prefixTable('log_conversion') . " - SET " . implode($updateParts, ', ') . " - WHERE " . implode($updateWhereParts, ' AND '); - Tracker::getDatabase()->query($sql, $sqlBind); - return true; - } else { - $sql = 'INSERT IGNORE INTO ' . Common::prefixTable('log_conversion') . " - ($fields) VALUES ($bindFields) "; - $bind = array_values($newGoal); - $result = Tracker::getDatabase()->query($sql, $bind); - - // If a record was inserted, we return true - return Tracker::getDatabase()->rowCount($result) > 0; - } + // If a record was inserted, we return true + return Tracker::getDatabase()->rowCount($result) > 0; } /** @@ -841,4 +822,22 @@ protected function getItemRowCast($row) (string)$row[self::INTERNAL_ITEM_QUANTITY], ); } + + protected function updateExistingConversion($newGoal, $updateWhere) + { + $updateParts = $sqlBind = $updateWhereParts = array(); + foreach ($newGoal AS $name => $value) { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + foreach ($updateWhere as $name => $value) { + $updateWhereParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + $sql = 'UPDATE ' . Common::prefixTable('log_conversion') . " + SET " . implode($updateParts, ', ') . " + WHERE " . implode($updateWhereParts, ' AND '); + Tracker::getDatabase()->query($sql, $sqlBind); + return true; + } } diff --git a/core/Tracker/PageUrl.php b/core/Tracker/PageUrl.php new file mode 100644 index 00000000000..37620cc7d47 --- /dev/null +++ b/core/Tracker/PageUrl.php @@ -0,0 +1,330 @@ + 1, + 'http://' => 0, + 'https://www.' => 3, + 'https://' => 2 + ); + + protected static $queryParametersToExclude = array('gclid', 'fb_xd_fragment', 'fb_comment_id', + 'phpsessid', 'jsessionid', 'sessionid', 'aspsessionid', + 'doing_wp_cron'); + + /** + * Given the Input URL, will exclude all query parameters set for this site + * Note: Site Search parameters are excluded in detectSiteSearch() + * @static + * @param $originalUrl + * @param $idSite + * @return bool|string + */ + public static function excludeQueryParametersFromUrl($originalUrl, $idSite) + { + $originalUrl = self::cleanupUrl($originalUrl); + + $parsedUrl = @parse_url($originalUrl); + $parsedUrl = self::cleanupHostAndHashTag($parsedUrl, $idSite); + $parametersToExclude = self::getQueryParametersToExclude($idSite); + + if (empty($parsedUrl['query'])) { + if (empty($parsedUrl['fragment'])) { + return UrlHelper::getParseUrlReverse($parsedUrl); + } + // Exclude from the hash tag as well + $queryParameters = UrlHelper::getArrayFromQueryString($parsedUrl['fragment']); + $parsedUrl['fragment'] = UrlHelper::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude); + $url = UrlHelper::getParseUrlReverse($parsedUrl); + return $url; + } + $queryParameters = UrlHelper::getArrayFromQueryString($parsedUrl['query']); + $parsedUrl['query'] = UrlHelper::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude); + $url = UrlHelper::getParseUrlReverse($parsedUrl); + return $url; + } + + + /** + * Returns the array of parameters names that must be excluded from the Query String in all tracked URLs + * @static + * @param $idSite + * @return array + */ + public static function getQueryParametersToExclude($idSite) + { + $campaignTrackingParameters = Common::getCampaignParameters(); + + $campaignTrackingParameters = array_merge( + $campaignTrackingParameters[0], // campaign name parameters + $campaignTrackingParameters[1] // campaign keyword parameters + ); + + $website = Cache::getCacheWebsiteAttributes($idSite); + $excludedParameters = isset($website['excluded_parameters']) + ? $website['excluded_parameters'] + : array(); + + if (!empty($excludedParameters)) { + Common::printDebug('Excluding parameters "' . implode(',', $excludedParameters) . '" from URL'); + } + + $parametersToExclude = array_merge($excludedParameters, + self::$queryParametersToExclude, + $campaignTrackingParameters); + + $parametersToExclude = array_map('strtolower', $parametersToExclude); + return $parametersToExclude; + } + + + /** + * Returns true if URL fragments should be removed for a specific site, + * false if otherwise. + * + * This function uses the Tracker cache and not the MySQL database. + * + * @param $idSite int The ID of the site to check for. + * @return bool + */ + public static function shouldRemoveURLFragmentFor($idSite) + { + $websiteAttributes = Cache::getCacheWebsiteAttributes($idSite); + return !$websiteAttributes['keep_url_fragment']; + } + + /** + * Cleans and/or removes the URL fragment of a URL. + * + * @param $urlFragment string The URL fragment to process. + * @param $idSite int|bool If not false, this function will check if URL fragments + * should be removed for the site w/ this ID and if so, + * the returned processed fragment will be empty. + * + * @return string The processed URL fragment. + */ + public static function processUrlFragment($urlFragment, $idSite = false) + { + // if we should discard the url fragment for this site, return an empty string as + // the processed url fragment + if ($idSite !== false + && PageUrl::shouldRemoveURLFragmentFor($idSite) + ) { + return ''; + } else { + // Remove trailing Hash tag in ?query#hash# + if (substr($urlFragment, -1) == '#') { + $urlFragment = substr($urlFragment, 0, strlen($urlFragment) - 1); + } + return $urlFragment; + } + } + + /** + * Will cleanup the hostname (some browser do not strolower the hostname), + * and deal ith the hash tag on incoming URLs based on website setting. + * + * @param $parsedUrl + * @param $idSite int|bool The site ID of the current visit. This parameter is + * only used by the tracker to see if we should remove + * the URL fragment for this site. + * @return array + */ + protected static function cleanupHostAndHashTag($parsedUrl, $idSite = false) + { + if (empty($parsedUrl)) { + return $parsedUrl; + } + if (!empty($parsedUrl['host'])) { + $parsedUrl['host'] = mb_strtolower($parsedUrl['host'], 'UTF-8'); + } + + if (!empty($parsedUrl['fragment'])) { + $parsedUrl['fragment'] = PageUrl::processUrlFragment($parsedUrl['fragment'], $idSite); + } + + return $parsedUrl; + } + + /** + * Converts Matrix URL format + * from http://example.org/thing;paramA=1;paramB=6542 + * to http://example.org/thing?paramA=1¶mB=6542 + * + * @param string $originalUrl + * @return string + */ + public static function convertMatrixUrl($originalUrl) + { + $posFirstSemiColon = strpos($originalUrl, ";"); + if ($posFirstSemiColon === false) { + return $originalUrl; + } + $posQuestionMark = strpos($originalUrl, "?"); + $replace = ($posQuestionMark === false); + if ($posQuestionMark > $posFirstSemiColon) { + $originalUrl = substr_replace($originalUrl, ";", $posQuestionMark, 1); + $replace = true; + } + if ($replace) { + $originalUrl = substr_replace($originalUrl, "?", strpos($originalUrl, ";"), 1); + $originalUrl = str_replace(";", "&", $originalUrl); + } + return $originalUrl; + } + + /** + * Clean up string contents (filter, truncate, ...) + * + * @param string $string Dirty string + * @return string + */ + public static function cleanupString($string) + { + $string = trim($string); + $string = str_replace(array("\n", "\r", "\0"), '', $string); + + $limit = Config::getInstance()->Tracker['page_maximum_length']; + $clean = substr($string, 0, $limit); + return $clean; + } + + protected static function reencodeParameterValue($value, $encoding) + { + if (is_string($value)) { + $decoded = urldecode($value); + if (@mb_check_encoding($decoded, $encoding)) { + $value = urlencode(mb_convert_encoding($decoded, 'UTF-8', $encoding)); + } + } + return $value; + } + + protected static function reencodeParametersArray($queryParameters, $encoding) + { + foreach ($queryParameters as &$value) { + if (is_array($value)) { + $value = self::reencodeParametersArray($value, $encoding); + } else { + $value = PageUrl::reencodeParameterValue($value, $encoding); + } + } + return $queryParameters; + } + + /** + * Checks if query parameters are of a non-UTF-8 encoding and converts the values + * from the specified encoding to UTF-8. + * This method is used to workaround browser/webapp bugs (see #3450). When + * browsers fail to encode query parameters in UTF-8, the tracker will send the + * charset of the page viewed and we can sometimes work around invalid data + * being stored. + * + * @param array $queryParameters Name/value mapping of query parameters. + * @param bool|string $encoding of the HTML page the URL is for. Used to workaround + * browser bugs & mis-coded webapps. See #3450. + * + * @return array + */ + public static function reencodeParameters(&$queryParameters, $encoding = false) + { + // if query params are encoded w/ non-utf8 characters (due to browser bug or whatever), + // encode to UTF-8. + if ($encoding !== false + && strtolower($encoding) != 'utf-8' + && function_exists('mb_check_encoding') + ) { + $queryParameters = PageUrl::reencodeParametersArray($queryParameters, $encoding); + } + return $queryParameters; + } + + public static function cleanupUrl($url) + { + $url = Common::unsanitizeInputValue($url); + $url = PageUrl::cleanupString($url); + $url = PageUrl::convertMatrixUrl($url); + return $url; + } + + /** + * Build the full URL from the prefix ID and the rest. + * + * @param string $url + * @param integer $prefixId + * @return string + */ + public static function reconstructNormalizedUrl($url, $prefixId) + { + $map = array_flip(self::$urlPrefixMap); + if ($prefixId !== null && isset($map[$prefixId])) { + $fullUrl = $map[$prefixId] . $url; + } else { + $fullUrl = $url; + } + + // Clean up host & hash tags, for URLs + $parsedUrl = @parse_url($fullUrl); + $parsedUrl = PageUrl::cleanupHostAndHashTag($parsedUrl); + $url = UrlHelper::getParseUrlReverse($parsedUrl); + if (!empty($url)) { + return $url; + } + + return $fullUrl; + } + + /** + * Extract the prefix from a URL. + * Return the prefix ID and the rest. + * + * @param string $url + * @return array + */ + public static function normalizeUrl($url) + { + foreach (self::$urlPrefixMap as $prefix => $id) { + if (strtolower(substr($url, 0, strlen($prefix))) == $prefix) { + return array( + 'url' => substr($url, strlen($prefix)), + 'prefixId' => $id + ); + } + } + return array('url' => $url, 'prefixId' => null); + } + + public static function getUrlIfLookValid($url) + { + $url = PageUrl::cleanupString($url); + + if (!UrlHelper::isLookLikeUrl($url)) { + Common::printDebug("WARNING: URL looks invalid and is discarded"); + $url = ''; + return $url; + } + return $url; + } +} + diff --git a/core/Tracker/Referrer.php b/core/Tracker/Referrer.php index 09501943ce4..d646745c87b 100644 --- a/core/Tracker/Referrer.php +++ b/core/Tracker/Referrer.php @@ -74,7 +74,7 @@ public function getReferrerInformation($referrerUrl, $currentUrl, $idSite) $referrerUrl = ''; } - $currentUrl = Action::cleanupUrl($currentUrl); + $currentUrl = PageUrl::cleanupUrl($currentUrl); $this->referrerUrl = $referrerUrl; $this->referrerUrlParse = @parse_url($this->referrerUrl); diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php index b2b8b6d4efa..29da90dd893 100644 --- a/core/Tracker/Visit.php +++ b/core/Tracker/Visit.php @@ -433,7 +433,7 @@ protected function handleNewVisit($idActionUrl, $idActionName, $actionType, $vis 'visit_exit_idaction_url' => (int)$idActionUrl, 'visit_exit_idaction_name' => (int)$idActionName, 'visit_total_actions' => in_array($actionType, - array(Action::TYPE_ACTION_URL, + array(Action::TYPE_PAGE_URL, Action::TYPE_DOWNLOAD, Action::TYPE_OUTLINK, Action::TYPE_SITE_SEARCH)) diff --git a/core/Updates/2.0-b2.php b/core/Updates/2.0-b2.php new file mode 100644 index 00000000000..1de9ae2cfc3 --- /dev/null +++ b/core/Updates/2.0-b2.php @@ -0,0 +1,39 @@ + 1060, + + 'ALTER TABLE ' . Common::prefixTable('log_link_visit_action') + . " ADD COLUMN idaction_event_category INTEGER(10) UNSIGNED AFTER idaction_name_ref, + ADD COLUMN idaction_event_action INTEGER(10) UNSIGNED AFTER idaction_event_category" => 1060, + ); + } + + static function update() + { + Updater::updateDatabase(__FILE__, self::getSql()); + } +} diff --git a/core/Version.php b/core/Version.php index a7014bc09e7..18bd6437166 100644 --- a/core/Version.php +++ b/core/Version.php @@ -24,5 +24,5 @@ final class Version * Current Piwik version * @var string */ - const VERSION = '2.0-b1'; + const VERSION = '2.0-b2'; } diff --git a/libs/PiwikTracker/PiwikTracker.php b/libs/PiwikTracker/PiwikTracker.php index ebfdb2e0bae..262bfdeec57 100644 --- a/libs/PiwikTracker/PiwikTracker.php +++ b/libs/PiwikTracker/PiwikTracker.php @@ -79,6 +79,7 @@ function __construct($idSite, $apiUrl = '') $this->plugins = false; $this->visitorCustomVar = false; $this->pageCustomVar = false; + $this->eventCustomVar = false; $this->customData = false; $this->forcedDatetime = false; $this->token_auth = false; @@ -185,7 +186,7 @@ public function setAttributionInfo($jsonEncoded) * @param int $id Custom variable slot ID from 1-5 * @param string $name Custom variable name * @param string $value Custom variable value - * @param string $scope Custom variable scope. Possible values: visit, page + * @param string $scope Custom variable scope. Possible values: visit, page, event * @throws Exception */ public function setCustomVariable($id, $name, $value, $scope = 'visit') @@ -195,6 +196,8 @@ public function setCustomVariable($id, $name, $value, $scope = 'visit') } if ($scope == 'page') { $this->pageCustomVar[$id] = array($name, $value); + } elseif($scope == 'event') { + $this->eventCustomVar[$id] = array($name, $value); } elseif ($scope == 'visit') { $this->visitorCustomVar[$id] = array($name, $value); } else { @@ -203,13 +206,12 @@ public function setCustomVariable($id, $name, $value, $scope = 'visit') } /** - * Returns the currently assigned Custom Variable stored in a first party cookie. + * Returns the currently assigned Custom Variable. * - * This function will only work if the user is initiating the current request, and his cookies - * can be read by PHP from the $_COOKIE array. + * If scope is 'visit', it will attempt to read the value set in the first party cookie created by Piwik Tracker ($_COOKIE array). * * @param int $id Custom Variable integer index to fetch from cookie. Should be a value from 1 to 5 - * @param string $scope Custom variable scope. Possible values: visit, page + * @param string $scope Custom variable scope. Possible values: visit, page, event * * @throws Exception * @return mixed An array with this format: array( 0 => CustomVariableName, 1 => CustomVariableValue ) or false @@ -219,6 +221,8 @@ public function getCustomVariable($id, $scope = 'visit') { if ($scope == 'page') { return isset($this->pageCustomVar[$id]) ? $this->pageCustomVar[$id] : false; + } elseif ($scope == 'event') { + return isset($this->eventCustomVar[$id]) ? $this->eventCustomVar[$id] : false; } else if ($scope != 'visit') { throw new Exception("Invalid 'scope' parameter value"); } @@ -365,6 +369,18 @@ public function doTrackPageView($documentTitle) return $this->sendRequest($url); } + /** + * Tracks a page view + * + * @param string $documentTitle Page title as it will appear in the Actions > Page titles report + * @return mixed Response string or true if using bulk requests. + */ + public function doTrackEvent($category, $action, $name = false, $value = false) + { + $url = $this->getUrlTrackEvent($category, $action, $name, $value); + return $this->sendRequest($url); + } + /** * Tracks an internal Site Search query, and optionally tracks the Search Category, and Search results Count. * These are used to populate reports in Actions > Site Search. @@ -630,6 +646,38 @@ public function getUrlTrackPageView($documentTitle = '') return $url; } + /** + * Builds URL to track a custom event. + * + * @see doTrackEvent() + * @param string $category (optional) The Event Category (Videos, Music, Games...) + * @param string $action The Event's Action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...) + * @param string $name The Event's object Name (a particular Movie name, or Song name, or File name...) + * @param float $value The Event's value + * @return string URL to piwik.php with all parameters set to track the pageview + */ + public function getUrlTrackEvent($category, $action, $name = false, $value = false) + { + $url = $this->getRequest($this->idSite); + if(strlen($category) == 0) { + throw new Exception("You must specify an Event Category name (Music, Videos, Games...)."); + } + if(strlen($action) == 0) { + throw new Exception("You must specify an Event action (click, view, add...)."); + } + + $url .= '&e_c=' . urlencode($category); + $url .= '&e_a=' . urlencode($action); + + if(strlen($name) > 0) { + $url .= '&e_n=' . urlencode($name); + } + if(strlen($value) > 0) { + $url .= '&e_v=' . $value; + } + return $url; + } + /** * Builds URL to track a site search. * @@ -1061,6 +1109,7 @@ protected function getRequest($idSite) (!empty($this->customData) ? '&data=' . $this->customData : '') . (!empty($this->visitorCustomVar) ? '&_cvar=' . urlencode(json_encode($this->visitorCustomVar)) : '') . (!empty($this->pageCustomVar) ? '&cvar=' . urlencode(json_encode($this->pageCustomVar)) : '') . + (!empty($this->eventCustomVar) ? '&e_cvar=' . urlencode(json_encode($this->eventCustomVar)) : '') . (!empty($this->generationTime) ? '>_ms=' . ((int)$this->generationTime) : '') . // URL parameters @@ -1089,6 +1138,7 @@ protected function getRequest($idSite) $this->DEBUG_APPEND_URL; // Reset page level custom variables after this page view $this->pageCustomVar = false; + $this->eventCustomVar = false; return $url; } diff --git a/plugins/Actions/API.php b/plugins/Actions/API.php index 84ceca80acb..1251073d944 100644 --- a/plugins/Actions/API.php +++ b/plugins/Actions/API.php @@ -14,13 +14,14 @@ use Piwik\Archive; use Piwik\Common; use Piwik\DataTable; -use Piwik\Date; +use Piwik\Date; use Piwik\Metrics; -use Piwik\Piwik; +use Piwik\Piwik; use Piwik\Plugins\CustomVariables\API as APICustomVariables; use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; /** * The Actions API lets you request reports for all your Visitor Actions: Page URLs, Page titles (Piwik Events), @@ -199,7 +200,7 @@ public function getExitPageUrls($idSite, $period, $date, $segment = false, $expa public function getPageUrl($pageUrl, $idSite, $period, $date, $segment = false) { $callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); - $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Action::TYPE_ACTION_URL); + $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Action::TYPE_PAGE_URL); $this->filterPageDatatable($dataTable); $this->filterActionsDataTable($dataTable); return $dataTable; @@ -240,7 +241,7 @@ public function getExitPageTitles($idSite, $period, $date, $segment = false, $ex public function getPageTitle($pageName, $idSite, $period, $date, $segment = false) { $callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false); - $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Action::TYPE_ACTION_NAME); + $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Action::TYPE_PAGE_TITLE); $this->filterPageDatatable($dataTable); $this->filterActionsDataTable($dataTable); return $dataTable; @@ -378,12 +379,12 @@ protected function getFilterPageDatatableSearch($callBackParameters, $search, $a { if ($searchTree === false) { // build the query parts that are searched inside the tree - if ($actionType == Action::TYPE_ACTION_NAME) { + if ($actionType == Action::TYPE_PAGE_TITLE) { $searchedString = Common::unsanitizeInputValue($search); } else { $idSite = $callBackParameters[1]; try { - $searchedString = Action::excludeQueryParametersFromUrl($search, $idSite); + $searchedString = PageUrl::excludeQueryParametersFromUrl($search, $idSite); } catch (Exception $e) { $searchedString = $search; } diff --git a/plugins/Actions/Actions.php b/plugins/Actions/Actions.php index 84bae607b7d..9a3ef201f1f 100644 --- a/plugins/Actions/Actions.php +++ b/plugins/Actions/Actions.php @@ -165,7 +165,7 @@ public function getIdActionFromSegment($valueToMatch, $sqlField, $matchType, $se { $actionType = $this->guessActionTypeFromSegment($segmentName); - if ($actionType == Action::TYPE_ACTION_URL) { + if ($actionType == Action::TYPE_PAGE_URL) { // for urls trim protocol and www because it is not recorded in the db $valueToMatch = preg_replace('@^http[s]?://(www\.)?@i', '', $valueToMatch); } @@ -632,10 +632,10 @@ static protected function isCustomVariablesPluginsEnabled() protected function guessActionTypeFromSegment($segmentName) { if (stripos($segmentName, 'pageurl') !== false) { - $actionType = Action::TYPE_ACTION_URL; + $actionType = Action::TYPE_PAGE_URL; return $actionType; } elseif (stripos($segmentName, 'pagetitle') !== false) { - $actionType = Action::TYPE_ACTION_NAME; + $actionType = Action::TYPE_PAGE_TITLE; return $actionType; } elseif (stripos($segmentName, 'sitesearch') !== false) { $actionType = Action::TYPE_SITE_SEARCH; diff --git a/plugins/Actions/Archiver.php b/plugins/Actions/Archiver.php index 99cabed9b3e..6c8fcf5107b 100644 --- a/plugins/Actions/Archiver.php +++ b/plugins/Actions/Archiver.php @@ -10,9 +10,6 @@ */ namespace Piwik\Plugins\Actions; -use Piwik\Config; -use Piwik\DataTable\Manager; -use Piwik\DataTable\Row\DataTableSummaryRow; use Piwik\DataTable; use Piwik\Metrics; use Piwik\RankingQuery; @@ -55,10 +52,10 @@ class Archiver extends \Piwik\Plugin\Archiver ); public static $actionTypes = array( - Action::TYPE_ACTION_URL, + Action::TYPE_PAGE_URL, Action::TYPE_OUTLINK, Action::TYPE_DOWNLOAD, - Action::TYPE_ACTION_NAME, + Action::TYPE_PAGE_TITLE, Action::TYPE_SITE_SEARCH, ); static protected $invalidSummedColumnNameToRenamedNameFromPeriodArchive = array( @@ -66,7 +63,7 @@ class Archiver extends \Piwik\Plugin\Archiver Metrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => Metrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS, Metrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => Metrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS, ); - static protected $invalidSummedColumnNameToDeleteFromDayArchive = array( + static public $invalidSummedColumnNameToDeleteFromDayArchive = array( Metrics::INDEX_NB_UNIQ_VISITORS, Metrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS, Metrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS, @@ -91,34 +88,7 @@ function __construct($processor) */ public function archiveDay() { - $rankingQueryLimit = self::getRankingQueryLimit(); - - // FIXME: This is a quick fix for #3482. The actual cause of the bug is that - // the site search & performance metrics additions to - // ArchivingHelper::updateActionsTableWithRowQuery expect every - // row to have 'type' data, but not all of the SQL queries that are run w/o - // ranking query join on the log_action table and thus do not select the - // log_action.type column. - // - // NOTES: Archiving logic can be generalized as follows: - // 0) Do SQL query over log_link_visit_action & join on log_action to select - // some metrics (like visits, hits, etc.) - // 1) For each row, cache the action row & metrics. (This is done by - // updateActionsTableWithRowQuery for result set rows that have - // name & type columns.) - // 2) Do other SQL queries for metrics we can't put in the first query (like - // entry visits, exit vists, etc.) w/o joining log_action. - // 3) For each row, find the cached row by idaction & add the new metrics to - // it. (This is done by updateActionsTableWithRowQuery for result set rows - // that DO NOT have name & type columns.) - // - // The site search & performance metrics additions expect a 'type' all the time - // which breaks the original pre-rankingquery logic. Ranking query requires a - // join, so the bug is only seen when ranking query is disabled. - if ($rankingQueryLimit === 0) { - $rankingQueryLimit = 100000; - } - + $rankingQueryLimit = ArchivingHelper::getRankingQueryLimit(); ArchivingHelper::reloadConfig(); $this->initActionsTables(); @@ -132,26 +102,6 @@ public function archiveDay() return true; } - /** - * Returns the limit to use with RankingQuery for this plugin. - * - * @return int - */ - private static function getRankingQueryLimit() - { - $configGeneral = Config::getInstance()->General; - $configLimit = $configGeneral['archiving_ranking_query_row_limit']; - return $configLimit == 0 ? 0 : max( - $configLimit, - $configGeneral['datatable_archiving_maximum_rows_actions'], - $configGeneral['datatable_archiving_maximum_rows_subtable_actions'] - ); - } - - /* - * Page URLs and Page names, general stats - */ - /** * Initializes the DataTables created by the archiveDay function. */ @@ -162,8 +112,8 @@ private function initActionsTables() $dataTable = new DataTable(); $dataTable->setMaximumAllowedRows(ArchivingHelper::$maximumRowsInDataTableLevelZero); - if ($type == Action::TYPE_ACTION_URL - || $type == Action::TYPE_ACTION_NAME + if ($type == Action::TYPE_PAGE_URL + || $type == Action::TYPE_PAGE_TITLE ) { // for page urls and page titles, performance metrics exist and have to be aggregated correctly $dataTable->setColumnAggregationOperations(self::$actionColumnAggregationOperations); @@ -239,7 +189,10 @@ protected function archiveDayActions($rankingQueryLimit) // 2) For each page view, count number of times the referrer page was a Site Search if ($this->isSiteSearchEnabled()) { $selectFlagNoResultKeywords = ", - CASE WHEN (MAX(log_link_visit_action.custom_var_v" . Action::CVAR_INDEX_SEARCH_COUNT . ") = 0 AND log_link_visit_action.custom_var_k" . Action::CVAR_INDEX_SEARCH_COUNT . " = '" . Action::CVAR_KEY_SEARCH_COUNT . "') THEN 1 ELSE 0 END AS `" . Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT . "`"; + CASE WHEN (MAX(log_link_visit_action.custom_var_v" . Action::CVAR_INDEX_SEARCH_COUNT . ") = 0 + AND log_link_visit_action.custom_var_k" . Action::CVAR_INDEX_SEARCH_COUNT . " = '" . Action::CVAR_KEY_SEARCH_COUNT . "') + THEN 1 ELSE 0 END + AS `" . Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT . "`"; //we need an extra JOIN to know whether the referrer "idaction_name_ref" was a Site Search request $from[] = array( @@ -248,11 +201,13 @@ protected function archiveDayActions($rankingQueryLimit) "joinOn" => "log_link_visit_action.idaction_name_ref = log_action_name_ref.idaction" ); - $selectSiteSearchFollowingPages = ", - SUM(CASE WHEN log_action_name_ref.type = " . Action::TYPE_SITE_SEARCH . " THEN 1 ELSE 0 END) AS `" . Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS . "`"; + $selectPageIsFollowingSiteSearch = ", + SUM( CASE WHEN log_action_name_ref.type = " . Action::TYPE_SITE_SEARCH. " + THEN 1 ELSE 0 END) + AS `" . Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS . "`"; $select .= $selectFlagNoResultKeywords - . $selectSiteSearchFollowingPages; + . $selectPageIsFollowingSiteSearch; } $this->archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, "idaction_name", $rankingQuery); @@ -267,9 +222,6 @@ protected function isSiteSearchEnabled() protected function archiveDayQueryProcess($select, $from, $where, $orderBy, $groupBy, $sprintfField, $rankingQuery = false) { - // idaction field needs to be set in select clause before calling getSelectQuery(). - // if a complex segmentation join is needed, the field needs to be propagated - // to the outer select. therefore, $segment needs to know about it. $select = sprintf($select, $sprintfField); // get query with segmentation @@ -446,7 +398,7 @@ protected function recordDayReports() protected function recordPageUrlsReports() { - $dataTable = $this->getDataTable(Action::TYPE_ACTION_URL); + $dataTable = $this->getDataTable(Action::TYPE_PAGE_URL); $this->recordDataTable($dataTable, self::PAGE_URLS_RECORD_NAME); $records = array( @@ -467,53 +419,13 @@ protected function getDataTable($typeId) return $this->actionsTablesByType[$typeId]; } - protected function recordDataTable($dataTable, $recordName) + protected function recordDataTable(DataTable $dataTable, $recordName) { - self::deleteInvalidSummedColumnsFromDataTable($dataTable); + ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($dataTable); $s = $dataTable->getSerialized(ArchivingHelper::$maximumRowsInDataTableLevelZero, ArchivingHelper::$maximumRowsInSubDataTable, ArchivingHelper::$columnToSortByBeforeTruncation); $this->getProcessor()->insertBlobRecord($recordName, $s); } - /** - * For rows which have subtables (eg. directories with sub pages), - * deletes columns which don't make sense when all values of sub pages are summed. - * - * @param $dataTable DataTable - */ - static public function deleteInvalidSummedColumnsFromDataTable($dataTable) - { - foreach ($dataTable->getRows() as $id => $row) { - if (($idSubtable = $row->getIdSubDataTable()) !== null - || $id === DataTable::ID_SUMMARY_ROW - ) { - if ($idSubtable !== null) { - $subtable = Manager::getInstance()->getTable($idSubtable); - self::deleteInvalidSummedColumnsFromDataTable($subtable); - } - - if ($row instanceof DataTableSummaryRow) { - $row->recalculate(); - } - - foreach (self::$invalidSummedColumnNameToDeleteFromDayArchive as $name) { - $row->deleteColumn($name); - } - } - } - - // And this as well - self::removeEmptyColumns($dataTable); - } - - static protected function removeEmptyColumns($dataTable) - { - // Delete all columns that have a value of zero - $dataTable->filter('ColumnDelete', array( - $columnsToRemove = array(Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS), - $columnsToKeep = array(), - $deleteIfZeroOnly = true - )); - } protected function recordDownloadsReports() { @@ -535,7 +447,7 @@ protected function recordOutlinksReports() protected function recordPageTitlesReports() { - $dataTable = $this->getDataTable(Action::TYPE_ACTION_NAME); + $dataTable = $this->getDataTable(Action::TYPE_PAGE_TITLE); $this->recordDataTable($dataTable, self::PAGE_TITLES_RECORD_NAME); } diff --git a/plugins/Actions/ArchivingHelper.php b/plugins/Actions/ArchivingHelper.php index dbe536c916b..e4eb4a313ef 100644 --- a/plugins/Actions/ArchivingHelper.php +++ b/plugins/Actions/ArchivingHelper.php @@ -12,11 +12,14 @@ use PDOStatement; use Piwik\Config; +use Piwik\DataTable\Manager; use Piwik\DataTable\Row; use Piwik\DataTable; +use Piwik\DataTable\Row\DataTableSummaryRow; use Piwik\Metrics; use Piwik\Piwik; use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; use Zend_Db_Statement; /** @@ -26,13 +29,12 @@ * * @package Actions */ - class ArchivingHelper { const OTHERS_ROW_KEY = ''; /** - * FIXME See FIXME related to this function at Archiver::archiveDay. + * Ideally this should use the DataArray object instead of custom data structure * * @param Zend_Db_Statement|PDOStatement $query * @param string|bool $fieldQueried @@ -44,7 +46,7 @@ static public function updateActionsTableWithRowQuery($query, $fieldQueried, & $ $rowsProcessed = 0; while ($row = $query->fetch()) { if (empty($row['idaction'])) { - $row['type'] = ($fieldQueried == 'idaction_url' ? Action::TYPE_ACTION_URL : Action::TYPE_ACTION_NAME); + $row['type'] = ($fieldQueried == 'idaction_url' ? Action::TYPE_PAGE_URL : Action::TYPE_PAGE_TITLE); // 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. @@ -59,13 +61,13 @@ static public function updateActionsTableWithRowQuery($query, $fieldQueried, & $ // eg. When there's at least one row in a report that does not have a URL, not having this would break HTML/PDF reports. $url = ''; if ($row['type'] == Action::TYPE_SITE_SEARCH - || $row['type'] == Action::TYPE_ACTION_NAME + || $row['type'] == Action::TYPE_PAGE_TITLE ) { $url = null; } elseif (!empty($row['name']) && $row['name'] != DataTable::LABEL_SUMMARY_ROW ) { - $url = Action::reconstructNormalizedUrl((string)$row['name'], $row['url_prefix']); + $url = PageUrl::reconstructNormalizedUrl((string)$row['name'], $row['url_prefix']); } if (isset($row['name']) @@ -121,8 +123,8 @@ static public function updateActionsTableWithRowQuery($query, $fieldQueried, & $ } } - if ($row['type'] != Action::TYPE_ACTION_URL - && $row['type'] != Action::TYPE_ACTION_NAME + if ($row['type'] != Action::TYPE_PAGE_URL + && $row['type'] != Action::TYPE_PAGE_TITLE ) { // only keep performance metrics when they're used (i.e. for URLs and page titles) if (array_key_exists(Metrics::INDEX_PAGE_SUM_TIME_GENERATION, $row)) { @@ -174,6 +176,91 @@ static public function updateActionsTableWithRowQuery($query, $fieldQueried, & $ return $rowsProcessed; } + public static function removeEmptyColumns($dataTable) + { + // Delete all columns that have a value of zero + $dataTable->filter('ColumnDelete', array( + $columnsToRemove = array(Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS), + $columnsToKeep = array(), + $deleteIfZeroOnly = true + )); + } + + /** + * For rows which have subtables (eg. directories with sub pages), + * deletes columns which don't make sense when all values of sub pages are summed. + * + * @param $dataTable DataTable + */ + public static function deleteInvalidSummedColumnsFromDataTable($dataTable) + { + foreach ($dataTable->getRows() as $id => $row) { + if (($idSubtable = $row->getIdSubDataTable()) !== null + || $id === DataTable::ID_SUMMARY_ROW + ) { + if ($idSubtable !== null) { + $subtable = Manager::getInstance()->getTable($idSubtable); + self::deleteInvalidSummedColumnsFromDataTable($subtable); + } + + if ($row instanceof DataTableSummaryRow) { + $row->recalculate(); + } + + foreach (Archiver::$invalidSummedColumnNameToDeleteFromDayArchive as $name) { + $row->deleteColumn($name); + } + } + } + + // And this as well + ArchivingHelper::removeEmptyColumns($dataTable); + } + + /** + * Returns the limit to use with RankingQuery for this plugin. + * + * @return int + */ + public static function getRankingQueryLimit() + { + $configGeneral = Config::getInstance()->General; + $configLimit = $configGeneral['archiving_ranking_query_row_limit']; + $limit = $configLimit == 0 ? 0 : max( + $configLimit, + $configGeneral['datatable_archiving_maximum_rows_actions'], + $configGeneral['datatable_archiving_maximum_rows_subtable_actions'] + ); + + // FIXME: This is a quick fix for #3482. The actual cause of the bug is that + // the site search & performance metrics additions to + // ArchivingHelper::updateActionsTableWithRowQuery expect every + // row to have 'type' data, but not all of the SQL queries that are run w/o + // ranking query join on the log_action table and thus do not select the + // log_action.type column. + // + // NOTES: Archiving logic can be generalized as follows: + // 0) Do SQL query over log_link_visit_action & join on log_action to select + // some metrics (like visits, hits, etc.) + // 1) For each row, cache the action row & metrics. (This is done by + // updateActionsTableWithRowQuery for result set rows that have + // name & type columns.) + // 2) Do other SQL queries for metrics we can't put in the first query (like + // entry visits, exit vists, etc.) w/o joining log_action. + // 3) For each row, find the cached row by idaction & add the new metrics to + // it. (This is done by updateActionsTableWithRowQuery for result set rows + // that DO NOT have name & type columns.) + // + // The site search & performance metrics additions expect a 'type' all the time + // which breaks the original pre-rankingquery logic. Ranking query requires a + // join, so the bug is only seen when ranking query is disabled. + if ($limit === 0) { + $limit = 100000; + } + return $limit; + + } + /** * @param $columnName * @param $alreadyValue @@ -306,7 +393,7 @@ protected static function getActionRow($actionName, $actionType, $urlPrefix = nu * * @param string $name action name * @param int $type action type - * @param int $urlPrefix url prefix (only used for TYPE_ACTION_URL) + * @param int $urlPrefix url prefix (only used for TYPE_PAGE_URL) * @return array of exploded elements from $name */ static public function getActionExplodedNames($name, $type, $urlPrefix = null) @@ -354,14 +441,14 @@ static public function getActionExplodedNames($name, $type, $urlPrefix = null) } } - if ($type == Action::TYPE_ACTION_NAME) { + if ($type == Action::TYPE_PAGE_TITLE) { $categoryDelimiter = self::$actionTitleCategoryDelimiter; } else { $categoryDelimiter = self::$actionUrlCategoryDelimiter; } if ($isUrl) { - $urlFragment = Action::processUrlFragment($urlFragment); + $urlFragment = PageUrl::processUrlFragment($urlFragment); if (!empty($urlFragment)) { $name .= '#' . $urlFragment; } @@ -389,7 +476,7 @@ static public function getActionExplodedNames($name, $type, $urlPrefix = null) // we are careful to prefix the page URL / name with some value // so that if a page has the same name as a category // we don't merge both entries - if ($type != Action::TYPE_ACTION_NAME) { + if ($type != Action::TYPE_PAGE_TITLE) { $lastPageName = '/' . $lastPageName; } else { $lastPageName = ' ' . $lastPageName; @@ -434,7 +521,7 @@ static public function getUnknownActionName($type) self::$defaultActionNameWhenNotDefined = Piwik::translate('General_NotDefined', Piwik::translate('Actions_ColumnPageName')); self::$defaultActionUrlWhenNotDefined = Piwik::translate('General_NotDefined', Piwik::translate('Actions_ColumnPageURL')); } - if ($type == Action::TYPE_ACTION_NAME) { + if ($type == Action::TYPE_PAGE_TITLE) { return self::$defaultActionNameWhenNotDefined; } return self::$defaultActionUrlWhenNotDefined; diff --git a/plugins/Live/API.php b/plugins/Live/API.php index f414e74d751..882935bb7f9 100644 --- a/plugins/Live/API.php +++ b/plugins/Live/API.php @@ -28,8 +28,8 @@ use Piwik\Segment; use Piwik\Site; use Piwik\Tracker\Action; -use Piwik\Tracker; use Piwik\Tracker\GoalManager; +use Piwik\Tracker; /** * @see plugins/Live/Visitor.php @@ -841,7 +841,7 @@ private function enrichVisitorArrayWithActions($visitorDetailsArray, $actionsLim } // Reconstruct url from prefix - $actionDetail['url'] = Action::reconstructNormalizedUrl($actionDetail['url'], $actionDetail['url_prefix']); + $actionDetail['url'] = Tracker\PageUrl::reconstructNormalizedUrl($actionDetail['url'], $actionDetail['url_prefix']); unset($actionDetail['url_prefix']); // Set the time spent for this action (which is the timeSpentRef of the next action) diff --git a/plugins/Overlay/API.php b/plugins/Overlay/API.php index 334cd712b30..8e3e1c0e9a7 100644 --- a/plugins/Overlay/API.php +++ b/plugins/Overlay/API.php @@ -18,7 +18,7 @@ use Piwik\Plugins\SitesManager\API as APISitesManager; use Piwik\Plugins\SitesManager\SitesManager; use Piwik\Plugins\Transitions\API as APITransitions; -use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; class API extends \Piwik\Plugin\API { @@ -70,7 +70,7 @@ public function getFollowingPages($url, $idSite, $period, $date, $segment = fals { $this->authenticate($idSite); - $url = Action::excludeQueryParametersFromUrl($url, $idSite); + $url = PageUrl::excludeQueryParametersFromUrl($url, $idSite); // we don't unsanitize $url here. it will be done in the Transitions plugin. $resultDataTable = new DataTable; diff --git a/plugins/Overlay/Controller.php b/plugins/Overlay/Controller.php index eadc70b7f08..ee83bd05622 100644 --- a/plugins/Overlay/Controller.php +++ b/plugins/Overlay/Controller.php @@ -20,6 +20,7 @@ use Piwik\Plugins\SitesManager\API as APISitesManager; use Piwik\ProxyHttp; use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; use Piwik\View; class Controller extends \Piwik\Plugin\Controller @@ -57,12 +58,12 @@ public function renderSidebar() $currentUrl = Common::getRequestVar('currentUrl'); $currentUrl = Common::unsanitizeInputValue($currentUrl); - $normalizedCurrentUrl = Action::excludeQueryParametersFromUrl($currentUrl, $idSite); + $normalizedCurrentUrl = PageUrl::excludeQueryParametersFromUrl($currentUrl, $idSite); $normalizedCurrentUrl = Common::unsanitizeInputValue($normalizedCurrentUrl); // load the appropriate row of the page urls report using the label filter ArchivingHelper::reloadConfig(); - $path = ArchivingHelper::getActionExplodedNames($normalizedCurrentUrl, Action::TYPE_ACTION_URL); + $path = ArchivingHelper::getActionExplodedNames($normalizedCurrentUrl, Action::TYPE_PAGE_URL); $path = array_map('urlencode', $path); $label = implode('>', $path); $request = new Request( diff --git a/plugins/Transitions/API.php b/plugins/Transitions/API.php index f48ca9d9a75..793d46a1509 100644 --- a/plugins/Transitions/API.php +++ b/plugins/Transitions/API.php @@ -30,6 +30,7 @@ use Piwik\SegmentExpression; use Piwik\Site; use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; /** * @package Transitions @@ -153,7 +154,7 @@ private function deriveIdAction($actionName, $actionType) if ($id < 0) { $unknown = ArchivingHelper::getUnknownActionName( - Action::TYPE_ACTION_NAME); + Action::TYPE_PAGE_TITLE); if (trim($actionName) == trim($unknown)) { $id = $actionsPlugin->getIdActionFromSegment('', 'idaction_name', SegmentExpression::MATCH_EQUAL, 'pageTitle'); @@ -233,7 +234,7 @@ public function queryFollowingActions($idaction, $actionType, LogAggregator $log $isTitle = ($actionType == 'title'); if (!$isTitle) { // specific setup for page urls - $types[Action::TYPE_ACTION_URL] = 'followingPages'; + $types[Action::TYPE_PAGE_URL] = 'followingPages'; $dimension = 'IF( idaction_url IS NULL, idaction_name, idaction_url )'; // site search referrers are logged with url=NULL // when we find one, we have to join on name @@ -241,7 +242,7 @@ public function queryFollowingActions($idaction, $actionType, LogAggregator $log $selects = array('log_action.name', 'log_action.url_prefix', 'log_action.type'); } else { // specific setup for page titles: - $types[Action::TYPE_ACTION_NAME] = 'followingPages'; + $types[Action::TYPE_PAGE_TITLE] = 'followingPages'; // join log_action on name and url and pick depending on url type // the table joined on url is log_action1 $joinLogActionColumn = array('idaction_url', 'idaction_name'); @@ -250,7 +251,7 @@ public function queryFollowingActions($idaction, $actionType, LogAggregator $log ' /* following site search */ . ' WHEN log_link_visit_action.idaction_url IS NULL THEN log_action2.idaction ' /* following page view: use page title */ . ' - WHEN log_action1.type = ' . Action::TYPE_ACTION_URL . ' THEN log_action2.idaction + WHEN log_action1.type = ' . Action::TYPE_PAGE_URL . ' THEN log_action2.idaction ' /* following download or outlink: use url */ . ' ELSE log_action1.idaction END @@ -260,7 +261,7 @@ public function queryFollowingActions($idaction, $actionType, LogAggregator $log ' /* following site search */ . ' WHEN log_link_visit_action.idaction_url IS NULL THEN log_action2.name ' /* following page view: use page title */ . ' - WHEN log_action1.type = ' . Action::TYPE_ACTION_URL . ' THEN log_action2.name + WHEN log_action1.type = ' . Action::TYPE_PAGE_URL . ' THEN log_action2.name ' /* following download or outlink: use url */ . ' ELSE log_action1.name END AS `name`', @@ -268,7 +269,7 @@ public function queryFollowingActions($idaction, $actionType, LogAggregator $log ' /* following site search */ . ' WHEN log_link_visit_action.idaction_url IS NULL THEN log_action2.type ' /* following page view: use page title */ . ' - WHEN log_action1.type = ' . Action::TYPE_ACTION_URL . ' THEN log_action2.type + WHEN log_action1.type = ' . Action::TYPE_PAGE_URL . ' THEN log_action2.type ' /* following download or outlink: use url */ . ' ELSE log_action1.type END AS `type`', @@ -415,12 +416,12 @@ protected function queryInternalReferrers($idaction, $actionType, $logAggregator $rankingQuery->partitionResultIntoMultipleGroups('action_partition', array(0, 1, 2)); $type = $this->getColumnTypeSuffix($actionType); - $mainActionType = Action::TYPE_ACTION_URL; + $mainActionType = Action::TYPE_PAGE_URL; $dimension = 'idaction_url_ref'; $isTitle = $actionType == 'title'; if ($isTitle) { - $mainActionType = Action::TYPE_ACTION_NAME; + $mainActionType = Action::TYPE_PAGE_TITLE; $dimension = 'idaction_name_ref'; } @@ -504,13 +505,13 @@ private function getPageLabel(&$pageRecord, $isTitle) $label = $pageRecord['name']; if (empty($label)) { $label = ArchivingHelper::getUnknownActionName( - Action::TYPE_ACTION_NAME); + Action::TYPE_PAGE_TITLE); } return $label; } else if ($this->returnNormalizedUrls) { return $pageRecord['name']; } else { - return Action::reconstructNormalizedUrl( + return PageUrl::reconstructNormalizedUrl( $pageRecord['name'], $pageRecord['url_prefix']); } } diff --git a/tests/PHPUnit/BaseFixture.php b/tests/PHPUnit/BaseFixture.php index 3ab347fd31d..e05c6ad6c07 100644 --- a/tests/PHPUnit/BaseFixture.php +++ b/tests/PHPUnit/BaseFixture.php @@ -141,6 +141,7 @@ public static function getTracker($idSite, $dateTime, $defaultInit = true, $useL $t->setForceVisitDateTime($dateTime); if ($defaultInit) { + $t->setTokenAuth(self::getTokenAuth()); $t->setIp('156.5.3.2'); // Optional tracking diff --git a/tests/PHPUnit/Core/Tracker/ActionTest.php b/tests/PHPUnit/Core/Tracker/ActionTest.php index b358a4b04c5..b7f65897982 100644 --- a/tests/PHPUnit/Core/Tracker/ActionTest.php +++ b/tests/PHPUnit/Core/Tracker/ActionTest.php @@ -1,8 +1,9 @@ assertEquals($filteredUrl[0], Action::excludeQueryParametersFromUrl($url, $idSite)); + $this->assertEquals($filteredUrl[0], PageUrl::excludeQueryParametersFromUrl($url, $idSite)); } public function getTestUrlsHashtag() @@ -139,7 +140,7 @@ public function getTestUrlsHashtag() */ public function testRemoveTrailingHashtag($url, $expectedUrl) { - $this->assertEquals(Action::reconstructNormalizedUrl($url, Action::$urlPrefixMap['http://']), $expectedUrl); + $this->assertEquals(PageUrl::reconstructNormalizedUrl($url, PageUrl::$urlPrefixMap['http://']), $expectedUrl); } @@ -156,7 +157,7 @@ public function testExcludeQueryParametersSiteExcluded($url, $filteredUrl) $siteSearch = 1, $searchKeywordParameters = null, $searchCategoryParameters = null, $excludedIps = '', $excludedQueryParameters, $timezone = null, $currency = null, $group = null, $startDate = null, $excludedUserAgents = null, $keepURLFragments = 1); - $this->assertEquals($filteredUrl[1], Action::excludeQueryParametersFromUrl($url, $idSite)); + $this->assertEquals($filteredUrl[1], PageUrl::excludeQueryParametersFromUrl($url, $idSite)); } /** @@ -175,7 +176,7 @@ public function testExcludeQueryParametersSiteAndGlobalExcluded($url, $filteredU $excludedIps = '', $excludedQueryParameters, $timezone = null, $currency = null, $group = null, $startDate = null, $excludedUserAgents = null, $keepURLFragments = 1); API::getInstance()->setGlobalExcludedQueryParameters($excludedGlobalParameters); - $this->assertEquals($filteredUrl[1], Action::excludeQueryParametersFromUrl($url, $idSite)); + $this->assertEquals($filteredUrl[1], PageUrl::excludeQueryParametersFromUrl($url, $idSite)); } @@ -241,71 +242,71 @@ public function getExtractUrlData() 'request' => array('url' => 'http://example.org/'), 'expected' => array('name' => null, 'url' => 'http://example.org/', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), array( 'request' => array('url' => 'http://example.org/', 'action_name' => 'Example.org Website'), 'expected' => array('name' => 'Example.org Website', 'url' => 'http://example.org/', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), array( 'request' => array('url' => 'http://example.org/CATEGORY/'), 'expected' => array('name' => null, 'url' => 'http://example.org/CATEGORY/', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), array( 'request' => array('url' => 'http://example.org/CATEGORY/TEST', 'action_name' => 'Example.org / Category / test /'), 'expected' => array('name' => 'Example.org/Category/test', 'url' => 'http://example.org/CATEGORY/TEST', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), array( 'request' => array('url' => 'http://example.org/?2,123'), 'expected' => array('name' => null, 'url' => 'http://example.org/?2,123', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), // empty request array( 'request' => array(), 'expected' => array('name' => null, 'url' => '', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), array( 'request' => array('name' => null, 'url' => "\n"), 'expected' => array('name' => null, 'url' => '', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), array( 'request' => array('url' => 'http://example.org/category/', 'action_name' => 'custom name with/one delimiter/two delimiters/'), 'expected' => array('name' => 'custom name with/one delimiter/two delimiters', 'url' => 'http://example.org/category/', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), array( 'request' => array('url' => 'http://example.org/category/', 'action_name' => 'http://custom action name look like url/'), 'expected' => array('name' => 'http:/custom action name look like url', 'url' => 'http://example.org/category/', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), // testing: delete tab, trimmed, not strtolowered array( 'request' => array('url' => "http://example.org/category/test///test wOw "), 'expected' => array('name' => null, 'url' => 'http://example.org/category/test///test wOw', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), // testing: inclusion of zero values in action name array( 'request' => array('url' => "http://example.org/category/1/0/t/test"), 'expected' => array('name' => null, 'url' => 'http://example.org/category/1/0/t/test', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), // testing: action name ("Test …") - expect decoding of some html entities array( @@ -313,7 +314,7 @@ public function getExtractUrlData() 'action_name' => "Test …"), 'expected' => array('name' => 'Test …', 'url' => 'http://example.org/ACTION/URL', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), // testing: action name ("Special & chars") - expect no conversion of html special chars array( @@ -321,7 +322,7 @@ public function getExtractUrlData() 'action_name' => "Special & chars"), 'expected' => array('name' => 'Special & chars', 'url' => 'http://example.org/ACTION/URL', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), // testing: action name ("Tést") - handle wide character array( @@ -329,7 +330,7 @@ public function getExtractUrlData() 'action_name' => "Tést"), 'expected' => array('name' => 'Tést', 'url' => 'http://example.org/ACTION/URL', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), // testing: action name ("Tést") - handle UTF-8 byte sequence array( @@ -337,7 +338,7 @@ public function getExtractUrlData() 'action_name' => "T\xc3\xa9st"), 'expected' => array('name' => 'Tést', 'url' => 'http://example.org/ACTION/URL', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), // testing: action name ("Tést") - invalid UTF-8 (e.g., ISO-8859-1) is not handled array( @@ -345,7 +346,7 @@ public function getExtractUrlData() 'action_name' => "T\xe9st"), 'expected' => array('name' => version_compare(PHP_VERSION, '5.2.5') === -1 ? 'T\xe9st' : 'Tést', 'url' => 'http://example.org/ACTION/URL', - 'type' => Action::TYPE_ACTION_URL), + 'type' => Action::TYPE_PAGE_URL), ), ); } diff --git a/tests/PHPUnit/Fixtures/ManyVisitsWithGeoIP.php b/tests/PHPUnit/Fixtures/ManyVisitsWithGeoIP.php index 013d0ad0f65..edbe7b62f57 100644 --- a/tests/PHPUnit/Fixtures/ManyVisitsWithGeoIP.php +++ b/tests/PHPUnit/Fixtures/ManyVisitsWithGeoIP.php @@ -82,8 +82,8 @@ private function trackVisits($visitorCount, $setIp = false, $useLocal = true, $d $t = self::getTracker($idSite, $dateTime, $defaultInit = true, $useLocal); if ($doBulk) { $t->enableBulkTracking(); - $t->setTokenAuth(self::getTokenAuth()); } + $t->setTokenAuth(self::getTokenAuth()); for ($i = 0; $i != $visitorCount; ++$i) { $t->setVisitorId( substr(md5($i + $calledCounter * 1000), 0, $t::LENGTH_VISITOR_ID)); if ($setIp) { diff --git a/tests/PHPUnit/Fixtures/SomeVisitsManyPageviewsWithTransitions.php b/tests/PHPUnit/Fixtures/SomeVisitsManyPageviewsWithTransitions.php index db99a033197..cc8a20e8be7 100644 --- a/tests/PHPUnit/Fixtures/SomeVisitsManyPageviewsWithTransitions.php +++ b/tests/PHPUnit/Fixtures/SomeVisitsManyPageviewsWithTransitions.php @@ -38,7 +38,6 @@ private function setUpWebsitesAndGoals() private function trackVisits() { $tracker = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true); - $tracker->setTokenAuth(self::getTokenAuth()); $tracker->enableBulkTracking(); $tracker->setIp('156.5.3.1'); diff --git a/tests/PHPUnit/Fixtures/TwoSitesTwoVisitorsDifferentDays.php b/tests/PHPUnit/Fixtures/TwoSitesTwoVisitorsDifferentDays.php index b19c2b1bce3..1fb49286fac 100644 --- a/tests/PHPUnit/Fixtures/TwoSitesTwoVisitorsDifferentDays.php +++ b/tests/PHPUnit/Fixtures/TwoSitesTwoVisitorsDifferentDays.php @@ -85,7 +85,6 @@ private function trackVisits() // Second new visitor on Idsite 1: one page view $visitorB = self::getTracker($idSite, $dateTime, $defaultInit = true); $visitorB->enableBulkTracking(); - $visitorB->setTokenAuth(self::getTokenAuth()); $visitorB->setIp('100.52.156.83'); $visitorB->setResolution(800, 300); $visitorB->setForceVisitDateTime(Date::factory($dateTime)->addHour(1)->getDatetime()); diff --git a/tests/PHPUnit/Fixtures/TwoVisitsWithCustomEvents.php b/tests/PHPUnit/Fixtures/TwoVisitsWithCustomEvents.php new file mode 100644 index 00000000000..8ffd5b4c4a2 --- /dev/null +++ b/tests/PHPUnit/Fixtures/TwoVisitsWithCustomEvents.php @@ -0,0 +1,155 @@ +setUpWebsitesAndGoals(); + $this->trackVisits(); + } + + private function setUpWebsitesAndGoals() + { + // tests run in UTC, the Tracker in UTC + self::createWebsite($this->dateTime); + APIGoals::getInstance()->addGoal($this->idSite, 'triggered js', 'manually', '', ''); + } + + public function trackVisits() + { + $uselocal = false; + $vis = self::getTracker($this->idSite, $this->dateTime, $useDefault = true, $uselocal); + $this->moveTimeForward($vis); + + $this->trackMusicPlaying($vis); + $this->trackMusicRatings($vis); + $this->trackMovieWatchingIncludingInterval($vis); + + $this->dateTime = Date::factory($this->dateTime)->addHour(0.5); + $vis2 = self::getTracker($this->idSite, $this->dateTime, $useDefault = true, $uselocal); + $vis2->setIp('111.1.1.1'); + $vis2->setPlugins($flash = false, $java = false, $director = true); + + $this->trackMusicPlaying($vis2); + $this->trackMusicRatings($vis2); + $this->trackMovieWatchingIncludingInterval($vis2); + } + + private function moveTimeForward(PiwikTracker $vis, $minutes) + { + $hour = $minutes / 60; + return $vis->setForceVisitDateTime(Date::factory($this->dateTime)->addHour($hour)->getDatetime()); + } + + protected function trackMusicPlaying(PiwikTracker $vis) + { + $vis->setUrl('http://example.org/webradio'); + + $this->moveTimeForward($vis, 1); + $this->setMusicEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Music', 'play', 'La fiancée de l\'eau')); + + $this->moveTimeForward($vis, 2); + $this->setMusicEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Music', 'play25%', 'La fiancée de l\'eau')); + $this->moveTimeForward($vis, 3); + $this->setMusicEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Music', 'play50%', 'La fiancée de l\'eau')); + $this->moveTimeForward($vis, 4); + $this->setMusicEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Music', 'play75%', 'La fiancée de l\'eau')); + + $this->moveTimeForward($vis, 4.5); + $this->setMusicEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Music', 'playEnd', 'La fiancée de l\'eau')); + } + + protected function trackMusicRatings(PiwikTracker $vis) + { + $this->moveTimeForward($vis, 5); + $this->setMusicEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Music', 'rating', 'La fiancée de l\'eau', 9)); + + $this->moveTimeForward($vis, 5.02); + $this->setMusicEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Music', 'rating', 'La fiancée de l\'eau', 10)); + } + + protected function trackMovieWatchingIncludingInterval(PiwikTracker $vis) + { + $vis->setUrl('http://example.org/movies'); + + $this->moveTimeForward($vis, 30); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'playTrailer', 'Princess Mononoke (もののけ姫)')); + $this->moveTimeForward($vis, 33); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'playTrailer', 'Ponyo (崖の上のポニョ)')); + $this->moveTimeForward($vis, 35); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'playTrailer', 'Spirited Away (千と千尋の神隠し)')); + $this->moveTimeForward($vis, 36); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'clickBuyNow', 'Spirited Away (千と千尋の神隠し)')); + $this->moveTimeForward($vis, 38); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'playStart', 'Spirited Away (千と千尋の神隠し)')); + $this->moveTimeForward($vis, 60); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'play25%', 'Spirited Away (千と千尋の神隠し)')); + + // taking 2+ hours break & resuming this epic moment of cinema + $this->moveTimeForward($vis, 200); + + $this->moveTimeForward($vis, 222); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'play50%', 'Spirited Away (千と千尋の神隠し)')); + $this->moveTimeForward($vis, 244); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'play75%', 'Spirited Away (千と千尋の神隠し)')); + $this->moveTimeForward($vis, 266); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'playEnd', 'Spirited Away (千と千尋の神隠し)')); + $this->moveTimeForward($vis, 268); + $this->setMovieEventCustomVar($vis); + self::checkResponse($vis->doTrackEvent('Movie', 'rating', 'Spirited Away (千と千尋の神隠し)', 9.66)); + } + + private function setMusicEventCustomVar(PiwikTracker $vis) + { + $vis->setCustomVariable($id = 1, $name = 'Page Scope Custom var', $value = 'should not appear in events report', $scope = 'page'); + $vis->setCustomVariable($id = 1, $name = 'album', $value = 'En attendant les caravanes...', $scope = 'event'); + $vis->setCustomVariable($id = 1, $name = 'genre', $value = 'World music', $scope = 'event'); + } + + private function setMovieEventCustomVar(PiwikTracker $vis) + { + $vis->setCustomVariable($id = 1, $name = 'country', $value = '日本', $scope = 'event'); + $vis->setCustomVariable($id = 2, $name = 'genre', $value = 'Greatest animated films', $scope = 'event'); + $vis->setCustomVariable($id = 4, $name = 'genre', $value = 'Adventure', $scope = 'event'); + $vis->setCustomVariable($id = 5, $name = 'genre', $value = 'Family', $scope = 'event'); + $vis->setCustomVariable($id = 5, $name = 'movieid', $value = 15763, $scope = 'event'); + + $vis->setCustomVariable($id = 1, $name = 'Visit Scope Custom var', $value = 'should not appear in events report Bis', $scope = 'visit'); + } + + public function tearDown() + { + } + +} diff --git a/tests/PHPUnit/Integration/CustomEventsTest.php b/tests/PHPUnit/Integration/CustomEventsTest.php new file mode 100644 index 00000000000..8ab5b8e06be --- /dev/null +++ b/tests/PHPUnit/Integration/CustomEventsTest.php @@ -0,0 +1,85 @@ +runApiTests($api, $params); + } + + protected function getApiToCall() + { + return array( + 'Actions.get', + 'Live.getLastVisitsDetails', + 'Actions.getPageUrls', + ); + } + + protected function tearDown() + { + parent::tearDown(); + } + + public function getApiForTesting() + { + $dateTime = self::$fixture->dateTime; + $idSite1 = self::$fixture->idSite; + + $apiToCall = $this->getApiToCall(); + + $dayPeriod = 'day'; + $periods = array($dayPeriod, 'month'); + + $result = array( + array($apiToCall, array( + 'idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => $periods, + 'setDateLastN' => false, + 'testSuffix' => '')), + ); + + // testing metadata API for one metadata report + $apiToCall = array ( end($apiToCall) ); + + foreach ($apiToCall as $api) { + list($apiModule, $apiAction) = explode(".", $api); + + $result[] = array( + 'API.getProcessedReport', array('idSite' => $idSite1, + 'date' => $dateTime, + 'periods' => $dayPeriod, + 'setDateLastN' => true, + 'apiModule' => $apiModule, + 'apiAction' => $apiAction, + 'testSuffix' => '_' . $api . '_lastN') + ); + } + return $result; + } + + public static function getOutputPrefix() + { + return 'CustomEvents'; + } +} + +Test_Piwik_Integration_CustomEvents::$fixture = new Test_Piwik_Fixture_TwoVisitsWithCustomEvents(); + + diff --git a/tests/PHPUnit/Integration/UrlNormalizationTest.php b/tests/PHPUnit/Integration/UrlNormalizationTest.php index a31d061e8eb..709b5deb1da 100644 --- a/tests/PHPUnit/Integration/UrlNormalizationTest.php +++ b/tests/PHPUnit/Integration/UrlNormalizationTest.php @@ -91,7 +91,7 @@ public function testCheckPostConditions() $this->assertEquals($expected, $count, "only $expected actions expected"); $sql = "SELECT name, url_prefix FROM " . Common::prefixTable('log_action') - . " WHERE type = " . Action::TYPE_ACTION_URL + . " WHERE type = " . Action::TYPE_PAGE_URL . " ORDER BY idaction ASC"; $urls = Db::get()->fetchAll($sql); $expected = array( diff --git a/tests/PHPUnit/Integration/expected/test_CustomEvents_Actions.getPageUrls_lastN__API.getProcessedReport_day.xml b/tests/PHPUnit/Integration/expected/test_CustomEvents_Actions.getPageUrls_lastN__API.getProcessedReport_day.xml new file mode 100644 index 00000000000..e2f0dac23e4 --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_CustomEvents_Actions.getPageUrls_lastN__API.getProcessedReport_day.xml @@ -0,0 +1,86 @@ + + + Piwik test + 3 Jan 10 - 9 Jan 10 + + Actions + Page URLs + Actions + getPageUrls + Page URL + + Pageviews + Unique Pageviews + Bounce Rate + Avg. time on page + Exit rate + Avg. generation time + + + The number of times this page was visited. + The number of visits that included this page. If a page was viewed multiple times during one visit, it is only counted once. + The percentage of visits that started on this page and left the website straight away. + The average amount of time visitors spent on this page (only the page, not the entire website). + The percentage of visits that left the website after viewing this page. + The average time it took to generate the page. This metric includes the time it took the server to generate the web page, plus the time it took for the visitor to download the response from the server. A lower 'Avg. generation time' means a faster website for your visitors! + + This report contains information about the page URLs that have been visited. <br /> The table is organized hierarchically, the URLs are displayed as a folder structure.<br />Use the plus and minus icons on the left to navigate. + getPageUrls + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Actions&apiAction=getPageUrls&period=range&date=2010-01-03,2010-01-09 + index.php?module=API&method=ImageGraph.get&idSite=1&apiModule=Actions&apiAction=getPageUrls&period=day&date=2010-01-03,2010-01-09 + Actions_getPageUrls + + + + Pageviews + Unique Pageviews + Bounce Rate + Avg. time on page + Exit rate + Avg. generation time + + + + + + 4 + 20 + 00:38:00 + 0% + 100% + 0s + + + + 2 + 14 + 00:29:00 + 0% + 0% + 0s + + + + + + + + + + + + + http://example.org/movies + + + http://example.org/webradio + + + + + + + + + + \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.getPageUrls_day.xml b/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.getPageUrls_day.xml new file mode 100644 index 00000000000..9852ba7c180 --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.getPageUrls_day.xml @@ -0,0 +1,37 @@ + + + + + 4 + 2 + 20 + 9120 + 2 + 2 + 8 + 5522 + 0 + 2 + 4 + 2280 + 0% + 100% + http://example.org/movies + + + + 2 + 2 + 14 + 3480 + 2 + 2 + 26 + 7082 + 0 + 1740 + 0% + 0% + http://example.org/webradio + + \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.getPageUrls_month.xml b/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.getPageUrls_month.xml new file mode 100644 index 00000000000..b575bcaac45 --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.getPageUrls_month.xml @@ -0,0 +1,37 @@ + + + + + 4 + 20 + 9120 + 2 + 8 + 5522 + 0 + 4 + 2 + 2 + 2 + 2280 + 0% + 100% + http://example.org/movies + + + + 2 + 14 + 3480 + 2 + 26 + 7082 + 0 + 2 + 2 + 1740 + 0% + 0% + http://example.org/webradio + + \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.get_day.xml b/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.get_day.xml new file mode 100644 index 00000000000..ed299b9781f --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.get_day.xml @@ -0,0 +1,12 @@ + + + 34 + 6 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.get_month.xml b/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.get_month.xml new file mode 100644 index 00000000000..ed299b9781f --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_CustomEvents__Actions.get_month.xml @@ -0,0 +1,12 @@ + + + 34 + 6 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_CustomEvents__Live.getLastVisitsDetails_day.xml b/tests/PHPUnit/Integration/expected/test_CustomEvents__Live.getLastVisitsDetails_day.xml new file mode 100644 index 00000000000..e45b1c0bd63 --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_CustomEvents__Live.getLastVisitsDetails_day.xml @@ -0,0 +1,805 @@ + + + + 1 + 2 + 156.5.3.2 + 71d675ee7ed7fe75 + new + + 0 + + none + + 0 + 4 + + + action + http://example.org/movies + + 2 + 14 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 15 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 16 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 17 + + + + + + + Visit Scope Custom var + should not appear in events report Bis + + + 0 + USD + $ + + 12:34:06 + 12 + + + + + 2761 + 46 min 1s + 1 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/UserCountry/images/flags/fr.png + + + + France + + + Unknown + Unknown + http://piwik.org/faq/general/#faq_52 + direct + Direct Entry + + + + + + + Windows XP + WXP + Win XP + plugins/UserSettings/images/os/WXP.gif + gecko + Gecko (Firefox) + Firefox 3.6 + plugins/UserSettings/images/browsers/FF.gif + FF + 3.6 + normal + desktop + 1024x768 + plugins/UserSettings/images/screens/normal.gif + flash, java + + + plugins/UserSettings/images/plugins/flash.gif + flash + + + plugins/UserSettings/images/plugins/java.gif + java + + + + + + + + + + 1 + 1 + 156.5.3.2 + 71d675ee7ed7fe75 + new + + 0 + + none + + 0 + 13 + + + action + http://example.org/webradio + + 1 + 1 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 2 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 3 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 4 + + + + Page Scope Custom var + should not appear in events report + + + 30 + 30s + + + + action + http://example.org/webradio + + 1 + 5 + + + + Page Scope Custom var + should not appear in events report + + + 30 + 30s + + + + action + http://example.org/webradio + + 1 + 6 + + + + Page Scope Custom var + should not appear in events report + + + 1 + 1s + + + + action + http://example.org/webradio + + 1 + 7 + + + + Page Scope Custom var + should not appear in events report + + + 1499 + 24 min 59s + + + + action + http://example.org/movies + + 2 + 8 + + 180 + 3 min 0s + + + + action + http://example.org/movies + + 2 + 9 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 10 + + 60 + 1 min 0s + + + + action + http://example.org/movies + + 2 + 11 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 12 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 13 + + + + + + + Visit Scope Custom var + should not appear in events report Bis + + + 0 + USD + $ + + 12:34:06 + 12 + + + + + 3541 + 59 min 1s + 1 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/UserCountry/images/flags/fr.png + + + + France + + + Unknown + Unknown + http://piwik.org/faq/general/#faq_52 + direct + Direct Entry + + + + + + + Windows XP + WXP + Win XP + plugins/UserSettings/images/os/WXP.gif + gecko + Gecko (Firefox) + Firefox 3.6 + plugins/UserSettings/images/browsers/FF.gif + FF + 3.6 + normal + desktop + 1024x768 + plugins/UserSettings/images/screens/normal.gif + flash, java + + + plugins/UserSettings/images/plugins/flash.gif + flash + + + plugins/UserSettings/images/plugins/java.gif + java + + + + + + + + + + 1 + 4 + 111.1.1.1 + 3e5bba0b9eea4018 + new + + 0 + + none + + 0 + 4 + + + action + http://example.org/movies + + 2 + 31 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 32 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 33 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 34 + + + + + + + Visit Scope Custom var + should not appear in events report Bis + + + 0 + USD + $ + + 12:34:06 + 12 + + + + + 2761 + 46 min 1s + 1 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/UserCountry/images/flags/fr.png + + + + France + + + Unknown + Unknown + http://piwik.org/faq/general/#faq_52 + direct + Direct Entry + + + + + + + Windows XP + WXP + Win XP + plugins/UserSettings/images/os/WXP.gif + gecko + Gecko (Firefox) + Firefox 3.6 + plugins/UserSettings/images/browsers/FF.gif + FF + 3.6 + normal + desktop + 1024x768 + plugins/UserSettings/images/screens/normal.gif + director + + + plugins/UserSettings/images/plugins/director.gif + director + + + + + + + + + + 1 + 3 + 111.1.1.1 + 3e5bba0b9eea4018 + new + + 0 + + none + + 0 + 13 + + + action + http://example.org/webradio + + 1 + 18 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 19 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 20 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 21 + + + + Page Scope Custom var + should not appear in events report + + + 30 + 30s + + + + action + http://example.org/webradio + + 1 + 22 + + + + Page Scope Custom var + should not appear in events report + + + 30 + 30s + + + + action + http://example.org/webradio + + 1 + 23 + + + + Page Scope Custom var + should not appear in events report + + + 1 + 1s + + + + action + http://example.org/webradio + + 1 + 24 + + + + Page Scope Custom var + should not appear in events report + + + 1499 + 24 min 59s + + + + action + http://example.org/movies + + 2 + 25 + + 180 + 3 min 0s + + + + action + http://example.org/movies + + 2 + 26 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 27 + + 60 + 1 min 0s + + + + action + http://example.org/movies + + 2 + 28 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 29 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 30 + + + + + + + Visit Scope Custom var + should not appear in events report Bis + + + 0 + USD + $ + + 12:34:06 + 12 + + + + + 3541 + 59 min 1s + 1 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/UserCountry/images/flags/fr.png + + + + France + + + Unknown + Unknown + http://piwik.org/faq/general/#faq_52 + direct + Direct Entry + + + + + + + Windows XP + WXP + Win XP + plugins/UserSettings/images/os/WXP.gif + gecko + Gecko (Firefox) + Firefox 3.6 + plugins/UserSettings/images/browsers/FF.gif + FF + 3.6 + normal + desktop + 1024x768 + plugins/UserSettings/images/screens/normal.gif + director + + + plugins/UserSettings/images/plugins/director.gif + director + + + + + + + + + \ No newline at end of file diff --git a/tests/PHPUnit/Integration/expected/test_CustomEvents__Live.getLastVisitsDetails_month.xml b/tests/PHPUnit/Integration/expected/test_CustomEvents__Live.getLastVisitsDetails_month.xml new file mode 100644 index 00000000000..e45b1c0bd63 --- /dev/null +++ b/tests/PHPUnit/Integration/expected/test_CustomEvents__Live.getLastVisitsDetails_month.xml @@ -0,0 +1,805 @@ + + + + 1 + 2 + 156.5.3.2 + 71d675ee7ed7fe75 + new + + 0 + + none + + 0 + 4 + + + action + http://example.org/movies + + 2 + 14 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 15 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 16 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 17 + + + + + + + Visit Scope Custom var + should not appear in events report Bis + + + 0 + USD + $ + + 12:34:06 + 12 + + + + + 2761 + 46 min 1s + 1 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/UserCountry/images/flags/fr.png + + + + France + + + Unknown + Unknown + http://piwik.org/faq/general/#faq_52 + direct + Direct Entry + + + + + + + Windows XP + WXP + Win XP + plugins/UserSettings/images/os/WXP.gif + gecko + Gecko (Firefox) + Firefox 3.6 + plugins/UserSettings/images/browsers/FF.gif + FF + 3.6 + normal + desktop + 1024x768 + plugins/UserSettings/images/screens/normal.gif + flash, java + + + plugins/UserSettings/images/plugins/flash.gif + flash + + + plugins/UserSettings/images/plugins/java.gif + java + + + + + + + + + + 1 + 1 + 156.5.3.2 + 71d675ee7ed7fe75 + new + + 0 + + none + + 0 + 13 + + + action + http://example.org/webradio + + 1 + 1 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 2 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 3 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 4 + + + + Page Scope Custom var + should not appear in events report + + + 30 + 30s + + + + action + http://example.org/webradio + + 1 + 5 + + + + Page Scope Custom var + should not appear in events report + + + 30 + 30s + + + + action + http://example.org/webradio + + 1 + 6 + + + + Page Scope Custom var + should not appear in events report + + + 1 + 1s + + + + action + http://example.org/webradio + + 1 + 7 + + + + Page Scope Custom var + should not appear in events report + + + 1499 + 24 min 59s + + + + action + http://example.org/movies + + 2 + 8 + + 180 + 3 min 0s + + + + action + http://example.org/movies + + 2 + 9 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 10 + + 60 + 1 min 0s + + + + action + http://example.org/movies + + 2 + 11 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 12 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 13 + + + + + + + Visit Scope Custom var + should not appear in events report Bis + + + 0 + USD + $ + + 12:34:06 + 12 + + + + + 3541 + 59 min 1s + 1 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/UserCountry/images/flags/fr.png + + + + France + + + Unknown + Unknown + http://piwik.org/faq/general/#faq_52 + direct + Direct Entry + + + + + + + Windows XP + WXP + Win XP + plugins/UserSettings/images/os/WXP.gif + gecko + Gecko (Firefox) + Firefox 3.6 + plugins/UserSettings/images/browsers/FF.gif + FF + 3.6 + normal + desktop + 1024x768 + plugins/UserSettings/images/screens/normal.gif + flash, java + + + plugins/UserSettings/images/plugins/flash.gif + flash + + + plugins/UserSettings/images/plugins/java.gif + java + + + + + + + + + + 1 + 4 + 111.1.1.1 + 3e5bba0b9eea4018 + new + + 0 + + none + + 0 + 4 + + + action + http://example.org/movies + + 2 + 31 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 32 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 33 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 34 + + + + + + + Visit Scope Custom var + should not appear in events report Bis + + + 0 + USD + $ + + 12:34:06 + 12 + + + + + 2761 + 46 min 1s + 1 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/UserCountry/images/flags/fr.png + + + + France + + + Unknown + Unknown + http://piwik.org/faq/general/#faq_52 + direct + Direct Entry + + + + + + + Windows XP + WXP + Win XP + plugins/UserSettings/images/os/WXP.gif + gecko + Gecko (Firefox) + Firefox 3.6 + plugins/UserSettings/images/browsers/FF.gif + FF + 3.6 + normal + desktop + 1024x768 + plugins/UserSettings/images/screens/normal.gif + director + + + plugins/UserSettings/images/plugins/director.gif + director + + + + + + + + + + 1 + 3 + 111.1.1.1 + 3e5bba0b9eea4018 + new + + 0 + + none + + 0 + 13 + + + action + http://example.org/webradio + + 1 + 18 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 19 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 20 + + + + Page Scope Custom var + should not appear in events report + + + 60 + 1 min 0s + + + + action + http://example.org/webradio + + 1 + 21 + + + + Page Scope Custom var + should not appear in events report + + + 30 + 30s + + + + action + http://example.org/webradio + + 1 + 22 + + + + Page Scope Custom var + should not appear in events report + + + 30 + 30s + + + + action + http://example.org/webradio + + 1 + 23 + + + + Page Scope Custom var + should not appear in events report + + + 1 + 1s + + + + action + http://example.org/webradio + + 1 + 24 + + + + Page Scope Custom var + should not appear in events report + + + 1499 + 24 min 59s + + + + action + http://example.org/movies + + 2 + 25 + + 180 + 3 min 0s + + + + action + http://example.org/movies + + 2 + 26 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 27 + + 60 + 1 min 0s + + + + action + http://example.org/movies + + 2 + 28 + + 120 + 2 min 0s + + + + action + http://example.org/movies + + 2 + 29 + + 1320 + 22 min 0s + + + + action + http://example.org/movies + + 2 + 30 + + + + + + + Visit Scope Custom var + should not appear in events report Bis + + + 0 + USD + $ + + 12:34:06 + 12 + + + + + 3541 + 59 min 1s + 1 + 0 + 0 + 0 + Europe + eur + France + fr + plugins/UserCountry/images/flags/fr.png + + + + France + + + Unknown + Unknown + http://piwik.org/faq/general/#faq_52 + direct + Direct Entry + + + + + + + Windows XP + WXP + Win XP + plugins/UserSettings/images/os/WXP.gif + gecko + Gecko (Firefox) + Firefox 3.6 + plugins/UserSettings/images/browsers/FF.gif + FF + 3.6 + normal + desktop + 1024x768 + plugins/UserSettings/images/screens/normal.gif + director + + + plugins/UserSettings/images/plugins/director.gif + director + + + + + + + + + \ No newline at end of file diff --git a/tests/PHPUnit/Plugins/ActionsTest.php b/tests/PHPUnit/Plugins/ActionsTest.php index c7cbaa72d01..0c2507496f2 100644 --- a/tests/PHPUnit/Plugins/ActionsTest.php +++ b/tests/PHPUnit/Plugins/ActionsTest.php @@ -27,67 +27,67 @@ public function getActionNameTestData() { return array( array( - 'params' => array('name' => 'http://example.org/', 'type' => Action::TYPE_ACTION_URL, 'urlPrefix' => null), + 'params' => array('name' => 'http://example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => null), 'expected' => array('/index'), ), array( - 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_ACTION_URL, 'urlPrefix' => 1), + 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 1), 'expected' => array('/index'), ), array( - 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_ACTION_URL, 'urlPrefix' => 2), + 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 2), 'expected' => array('/index'), ), array( - 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_ACTION_URL, 'urlPrefix' => 3), + 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 3), 'expected' => array('/index'), ), array( - 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_ACTION_URL, 'urlPrefix' => 4), + 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 4), 'expected' => array('/index'), ), array( - 'params' => array('name' => 'example.org/path/', 'type' => Action::TYPE_ACTION_URL, 'urlPrefix' => 4), + 'params' => array('name' => 'example.org/path/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 4), 'expected' => array('path', '/index'), ), array( - 'params' => array('name' => 'example.org/test/path', 'type' => Action::TYPE_ACTION_URL, 'urlPrefix' => 1), + 'params' => array('name' => 'example.org/test/path', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 1), 'expected' => array('test', '/path'), ), array( - 'params' => array('name' => 'http://example.org/path/', 'type' => Action::TYPE_ACTION_URL), + 'params' => array('name' => 'http://example.org/path/', 'type' => Action::TYPE_PAGE_URL), 'expected' => array('path', '/index'), ), array( - 'params' => array('name' => 'example.org/test/path', 'type' => Action::TYPE_ACTION_URL, 'urlPrefix' => 1), + 'params' => array('name' => 'example.org/test/path', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 1), 'expected' => array('test', '/path'), ), array( - 'params' => array('name' => 'Test / Path', 'type' => Action::TYPE_ACTION_URL), + 'params' => array('name' => 'Test / Path', 'type' => Action::TYPE_PAGE_URL), 'expected' => array('Test', '/Path'), ), array( - 'params' => array('name' => ' Test trim ', 'type' => Action::TYPE_ACTION_URL), + 'params' => array('name' => ' Test trim ', 'type' => Action::TYPE_PAGE_URL), 'expected' => array('/Test trim'), ), array( - 'params' => array('name' => 'Category / Subcategory', 'type' => Action::TYPE_ACTION_NAME), + 'params' => array('name' => 'Category / Subcategory', 'type' => Action::TYPE_PAGE_TITLE), 'expected' => array('Category', ' Subcategory'), ), array( - 'params' => array('name' => '/path/index.php?var=test', 'type' => Action::TYPE_ACTION_NAME), + 'params' => array('name' => '/path/index.php?var=test', 'type' => Action::TYPE_PAGE_TITLE), 'expected' => array('path', ' index.php?var=test'), ), array( - 'params' => array('name' => 'http://example.org/path/Default.aspx#anchor', 'type' => Action::TYPE_ACTION_NAME), + 'params' => array('name' => 'http://example.org/path/Default.aspx#anchor', 'type' => Action::TYPE_PAGE_TITLE), 'expected' => array('path', ' Default.aspx#anchor'), ), array( - 'params' => array('name' => '', 'type' => Action::TYPE_ACTION_NAME), + 'params' => array('name' => '', 'type' => Action::TYPE_PAGE_TITLE), 'expected' => array('Page Name not defined'), ), array( - 'params' => array('name' => '', 'type' => Action::TYPE_ACTION_URL), + 'params' => array('name' => '', 'type' => Action::TYPE_PAGE_URL), 'expected' => array('Page URL not defined'), ), array(