| <?php |
|
|
| |
| |
| |
| |
| |
| |
|
|
| namespace Piwik\Plugins\Goals\Commands; |
|
|
| use Piwik\Common; |
| use Piwik\Container\StaticContainer; |
| use Piwik\Date; |
| use Piwik\Db; |
| use Piwik\Plugin\ConsoleCommand; |
| use Piwik\Plugins\Goals\Model as GoalsModel; |
| use Piwik\Plugins\SitesManager\Model as SitesModel; |
| use Piwik\Site; |
| use Piwik\Timer; |
| use Piwik\Tracker\GoalManager; |
| use Piwik\Updater; |
| use Piwik\Updater\Migration\Factory as MigrationFactory; |
|
|
| |
| |
| |
| class CalculateConversionPages extends ConsoleCommand |
| { |
| protected function configure() |
| { |
| $this->setName('core:calculate-conversion-pages'); |
| $this->setDescription('Calculate the pages before metric for historic conversions'); |
| $this->addOptionalValueOption('dates', null, 'Calculate for conversions in this date range. Eg, 2012-01-01,2013-01-01', null); |
| $this->addOptionalValueOption('last-n', null, 'Calculate just the last n conversions', 0); |
| $this->addOptionalValueOption( |
| 'idsite', |
| null, |
| 'Calculate for conversions belonging to the site with this ID. Comma separated list of website id. Eg, 1, 2, 3, etc. By default conversions from all sites are calculated.', |
| null |
| ); |
| $this->addOptionalValueOption('idgoal', null, 'Calculate conversions for this goal. A comma separated list of goal ids can be used only if a single site is specified. Eg, 1, 2, 3, etc. By default conversions for all goals are calculated.', null); |
| $this->addOptionalValueOption('force-recalc', null, 'Recalculate for conversions which already have a pages before value', 0); |
| } |
|
|
| protected function doExecute(): int |
| { |
| $dates = $this->getInput()->getOption('dates'); |
| $lastN = $this->getInput()->getOption('last-n'); |
| $forceRecalc = $this->getInput()->getOption('force-recalc'); |
| $idSite = $this->getSitesToCalculate(); |
| $idGoal = $this->getGoalsToCalculate(); |
|
|
| if (!$lastN && !$dates) { |
| throw new \InvalidArgumentException("No date range or last N option supplied. Calculating pages before for all conversions by default is not allowed, you must specify a date range using the --dates option or a last N count using the --last-n option"); |
| } |
|
|
| if ($lastN && $dates) { |
| throw new \InvalidArgumentException("The last N option cannot be used with a date range, please choose just one of these options"); |
| } |
|
|
| if (!is_numeric($lastN)) { |
| throw new \InvalidArgumentException("The last N option must be a number"); |
| } |
|
|
| $from = null; |
| $to = null; |
| if (!empty($dates)) { |
| [$from, $to] = $this->getDateRangeToCalculate($dates); |
| } |
|
|
| $output = $this->getOutput(); |
|
|
| $output->writeln(sprintf( |
| "<info>Preparing to calculate the pages before metric for %s conversions belonging to %s %sfor %s.</info>", |
| $lastN ? "the last " . $lastN : 'all', |
| $idSite ? "website $idSite" : "ALL websites", |
| !empty($dates) ? "between " . $from . " and " . $to . " " : '', |
| $idGoal ? "goal id $idGoal" : "ALL goals" |
| )); |
|
|
| $timer = new Timer(); |
|
|
| $queries = $this->getQueries($from, $to, $lastN, $idSite, $idGoal, $forceRecalc); |
|
|
| $totalCalculated = 0; |
| foreach ($queries as $query) { |
| try { |
| $result = Db::query($query['sql'], $query['bind']); |
| } catch (\Exception $ex) { |
| $output->writeln("Exception executing query " . $query['sql'] . " with parameters " . json_encode($query['bind'])); |
| throw $ex; |
| } |
|
|
| $calcCount = $result->rowCount(); |
| $totalCalculated += $calcCount; |
| $output->write("."); |
| } |
|
|
| $this->writeSuccessMessage("Successfully calculated the pages before metric for $totalCalculated conversions. <comment>{$timer}</comment>"); |
|
|
| return self::SUCCESS; |
| } |
|
|
| |
| |
| |
| |
| public static function calculateYesterdayAndToday(): void |
| { |
| $migration = StaticContainer::get(MigrationFactory::class); |
|
|
| $queries = self::getQueries( |
| Date::factory('yesterday')->getDatetime(), |
| Date::factory('today')->getEndOfDay()->getDatetime() |
| ); |
|
|
| $migrations = []; |
| foreach ($queries as $query) { |
| $migrations[] = $migration->db->boundSql($query['sql'], $query['bind']); |
| } |
|
|
| $updater = StaticContainer::get(Updater::class); |
| $updater->executeMigrations(__FILE__, $migrations); |
| } |
|
|
| |
| |
| |
| |
| |
| private function getDateRangeToCalculate(string $dates): ?array |
| { |
| $parts = explode(',', $dates); |
| $parts = array_map('trim', $parts); |
|
|
| if (count($parts) !== 2) { |
| throw new \InvalidArgumentException("Invalid date range supplied: $dates"); |
| } |
|
|
| [$start, $end] = $parts; |
|
|
| try { |
| |
| $dateObjects = [Date::factory($start), Date::factory($end)->getEndOfDay()]; |
| } catch (\Exception $ex) { |
| throw new \InvalidArgumentException("Invalid date range supplied: $dates (" . $ex->getMessage() . ")", $code = 0, $ex); |
| } |
|
|
| if ($dateObjects[0]->getTimestamp() > $dateObjects[1]->getTimestamp()) { |
| throw new \InvalidArgumentException("Invalid date range supplied: $dates (first date is older than the last date)"); |
| } |
|
|
| $dateObjects = [$dateObjects[0]->getDatetime(), $dateObjects[1]->getDatetime()]; |
|
|
| return $dateObjects; |
| } |
|
|
| |
| |
| |
| private function getSitesToCalculate(): ?string |
| { |
| $idSite = $this->getInput()->getOption('idsite'); |
|
|
| if (is_null($idSite)) { |
| return null; |
| } |
|
|
| $sites = explode(',', $idSite); |
| foreach ($sites as $id) { |
| |
| try { |
| new Site($id); |
| } catch (\Exception $ex) { |
| throw new \InvalidArgumentException("Invalid site ID: $id", $code = 0, $ex); |
| } |
| } |
|
|
| return $idSite; |
| } |
|
|
| |
| |
| |
| private function getGoalsToCalculate(): ?string |
| { |
| $idGoal = $this->getInput()->getOption('idgoal'); |
|
|
| if (is_null($idGoal)) { |
| return null; |
| } |
|
|
| |
| $idSite = $this->getInput()->getOption('idsite'); |
| if (!is_numeric($idSite) || strpos($idSite, ',') !== false) { |
| throw new \InvalidArgumentException("The goals parameter can only be used when a single website is specified using the idsite parameter", $code = 0); |
| } |
|
|
| $goals = explode(',', $idGoal); |
| $goalsModel = new GoalsModel(); |
|
|
| foreach ($goals as $id) { |
| |
| if (!$goalsModel->doesGoalExist($id, $idSite) && $id != GoalManager::IDGOAL_ORDER) { |
| throw new \InvalidArgumentException("Invalid goal ID: $id", $code = 0); |
| } |
| } |
|
|
| return $idGoal; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| private static function getQueries( |
| ?string $startDatetime, |
| ?string $endDatetime, |
| ?int $lastN = null, |
| ?string $idSite = null, |
| ?string $idGoal = null, |
| ?bool $forceRecalc = false |
| ): array { |
| |
| if ($idSite === null) { |
| |
| $sitesModel = new SitesModel(); |
| $sites = $sitesModel->getSitesId(); |
| } else { |
| |
| $sites = explode(',', $idSite); |
| } |
|
|
| if ($lastN) { |
| |
| |
| |
| $sql = " |
| SELECT MIN(s.t) FROM ( |
| SELECT c.server_time AS t |
| FROM `" . Common::prefixTable('log_conversion') . "` c |
| "; |
|
|
| $where = ''; |
| if (!$forceRecalc) { |
| $where .= " AND c.pageviews_before IS NULL"; |
| } |
|
|
| $bind = []; |
| if ($idGoal !== null) { |
| $where .= ' AND c.idgoal = ? '; |
| $bind[] = $idGoal; |
| } |
| if ($idSite !== null) { |
| $where .= ' AND c.idsite = ? '; |
| $bind[] = $idSite; |
| } |
|
|
| if ($where !== '') { |
| $sql .= ' WHERE ' . ltrim($where, 'AND '); |
| } |
|
|
| $sql .= " ORDER BY c.server_time DESC LIMIT " . $lastN . ") AS s"; |
|
|
| $result = Db::fetchOne($sql, $bind); |
|
|
| if (!$result) { |
| return []; |
| } |
|
|
| $startDatetime = $result; |
| $endDatetime = Date::factory('now')->getDatetime(); |
| } |
|
|
| |
| |
| |
| $startDateTimeForActions = Date::factory($startDatetime)->subDay(1)->getDatetime(); |
|
|
| $queries = []; |
| foreach ($sites as $site) { |
| $timezone = Site::getTimezoneFor($site); |
|
|
| if ($idGoal === null) { |
| |
| $gids = Db::fetchAll("SELECT idgoal FROM `" . Common::prefixTable('goal') . "` |
| WHERE idsite = ? AND deleted = 0", [$site]); |
| $goals = array_column($gids, 'idgoal'); |
|
|
| |
| if (Site::isEcommerceEnabledFor($site)) { |
| $goals[] = GoalManager::IDGOAL_ORDER; |
| } |
| } else { |
| |
| $goals = explode(',', $idGoal); |
| } |
|
|
| foreach ($goals as $goal) { |
| $where = ''; |
| if (!$forceRecalc) { |
| $where .= " AND c.pageviews_before IS NULL"; |
| } |
|
|
| $conversionsStartDate = Date::factory($startDatetime, $timezone)->getDateTime(); |
| $conversionsEndDate = Date::factory($endDatetime, $timezone)->getDateTime(); |
|
|
| $bind = [ |
| $site, |
| Date::factory($startDateTimeForActions, $timezone)->getDateTime(), |
| $conversionsEndDate, |
| $site, |
| $goal, |
| $conversionsStartDate, |
| $conversionsEndDate, |
| $site, |
| $goal, |
| $conversionsStartDate, |
| $conversionsEndDate, |
| ]; |
|
|
| $sql = " |
| UPDATE " . Common::prefixTable('log_conversion') . " c |
| LEFT JOIN ( |
| SELECT c.idvisit, c.idgoal, COUNT(a.idvisit) AS pagesbefore, c.idlink_va, c.server_time |
| FROM `" . Common::prefixTable('log_conversion') . "` c |
| LEFT JOIN ( |
| SELECT va.idvisit, va.server_time |
| FROM `" . Common::prefixTable('log_link_visit_action') . "` va |
| INNER JOIN `" . Common::prefixTable('log_action') . "` a ON a.idaction = va.idaction_url |
| WHERE a.type = 1 |
| AND va.idsite = ? |
| AND va.server_time >= ? |
| AND va.server_time <= ? |
| ORDER BY NULL |
| ) AS a ON a.idvisit = c.idvisit AND a.server_time <= c.server_time |
| WHERE c.idsite = ? |
| AND c.idgoal = ? |
| AND c.server_time >= ? |
| AND c.server_time <= ? |
| " . $where . " |
| GROUP BY a.idvisit |
| ORDER BY NULL |
| ) AS s ON s.idvisit = c.idvisit AND s.server_time <= c.server_time |
| SET c.pageviews_before = s.pagesbefore |
| WHERE c.idsite = ? |
| AND c.idgoal = ? |
| AND c.server_time >= ? |
| AND c.server_time <= ? |
| " . $where; |
|
|
| $queries[] = ['sql' => $sql, 'bind' => $bind]; |
| } |
| } |
|
|
| return $queries; |
| } |
| } |
|
|