Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthieu Aubry <matt@piwik.org>2015-02-09 07:01:36 +0300
committerMatthieu Aubry <matt@piwik.org>2015-02-09 07:01:36 +0300
commit369e9399818aac669c11984cd3fc1e27fe66be9a (patch)
tree48faa235611cdaadac24d22e22633b8b621392ae /plugins
parent08a1c977ab89f5014c476923e75167896a1ab86a (diff)
parent17f681e9ff3c20af15f0ad7211d93aa3a7161b88 (diff)
Merge pull request #7112 from piwik/6436_action_dupes
Refactor new action insertion code so duplicate actions will not exist + provide means to fix duplicates
Diffstat (limited to 'plugins')
-rw-r--r--plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php215
-rw-r--r--plugins/CoreAdminHome/Model/DuplicateActionRemover.php193
-rw-r--r--plugins/CoreAdminHome/tests/Fixture/DuplicateActions.php171
-rw-r--r--plugins/CoreAdminHome/tests/Integration/FixDuplicateActionsTest.php175
-rw-r--r--plugins/CoreAdminHome/tests/Integration/Model/DuplicateActionRemoverTest.php112
5 files changed, 866 insertions, 0 deletions
diff --git a/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php b/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php
new file mode 100644
index 0000000000..54b53afabe
--- /dev/null
+++ b/plugins/CoreAdminHome/Commands/FixDuplicateLogActions.php
@@ -0,0 +1,215 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\CoreAdminHome\Commands;
+
+use Piwik\Common;
+use Piwik\Container\StaticContainer;
+use Piwik\DataAccess\Actions;
+use Piwik\DataAccess\ArchiveInvalidator;
+use Piwik\Plugin\ConsoleCommand;
+use Piwik\Plugins\CoreAdminHome\Model\DuplicateActionRemover;
+use Piwik\Timer;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Finds duplicate actions rows in log_action and removes them. Fixes references to duplicate
+ * actions in the log_link_visit_action table, log_conversion table, and log_conversion_item
+ * table.
+ *
+ * Prior to version 2.11, there was a race condition in the tracker where it was possible for
+ * two or more actions with the same name and type to be inserted simultaneously. This resulted
+ * in inaccurate data. A Piwik database with this problem can be fixed using this class.
+ *
+ * With version 2.11 and above, it is still possible for duplicate actions to be inserted, but
+ * ONLY if the tracker's PHP process fails suddenly right after inserting an action. This is
+ * very rare, and even if it does happen, report data will not be affected, but the extra
+ * actions can be deleted w/ this class.
+ */
+class FixDuplicateLogActions extends ConsoleCommand
+{
+ /**
+ * Used to invalidate archives. Only used if $shouldInvalidateArchives is true.
+ *
+ * @var ArchiveInvalidator
+ */
+ private $archiveInvalidator;
+
+ /**
+ * DAO used to find duplicate actions in log_action and fix references to them in other tables.
+ *
+ * @var DuplicateActionRemover
+ */
+ private $duplicateActionRemover;
+
+ /**
+ * DAO used to remove actions from the log_action table.
+ *
+ * @var Actions
+ */
+ private $actionsAccess;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * Constructor.
+ *
+ * @param ArchiveInvalidator $invalidator
+ * @param DuplicateActionRemover $duplicateActionRemover
+ * @param Actions $actionsAccess
+ * @param LoggerInterface $logger
+ */
+ public function __construct(ArchiveInvalidator $invalidator = null, DuplicateActionRemover $duplicateActionRemover = null,
+ Actions $actionsAccess = null, LoggerInterface $logger = null)
+ {
+ parent::__construct();
+
+ if ($invalidator === null) {
+ $invalidator = new ArchiveInvalidator();
+ }
+ $this->archiveInvalidator = $invalidator;
+
+ if ($duplicateActionRemover === null) {
+ $duplicateActionRemover = new DuplicateActionRemover();
+ }
+ $this->duplicateActionRemover = $duplicateActionRemover;
+
+ if ($actionsAccess === null) {
+ $actionsAccess = new Actions();
+ }
+ $this->actionsAccess = $actionsAccess;
+
+ if ($logger === null) {
+ $logger = StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+ $this->logger = $logger;
+ }
+
+ protected function configure()
+ {
+ $this->setName('core:fix-duplicate-log-actions');
+ $this->addOption('invalidate-archives', null, InputOption::VALUE_NONE, "If supplied, archives for logs that use duplicate actions will be invalidated."
+ . " On the next cron archive run, the reports for those dates will be re-processed.");
+ $this->setDescription('Removes duplicates in the log action table and fixes references to the duplicates in '
+ . 'related tables. NOTE: This action can take a long time to run!');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $invalidateArchives = $input->getOption('invalidate-archives');
+
+ $timer = new Timer();
+
+ $duplicateActions = $this->duplicateActionRemover->getDuplicateIdActions();
+ if (empty($duplicateActions)) {
+ $output->writeln("Found no duplicate actions.");
+ return;
+ }
+
+ $output->writeln("<info>Found " . count($duplicateActions) . " actions with duplicates.</info>");
+
+ list($numberRemoved, $allArchivesAffected) = $this->fixDuplicateActionReferences($duplicateActions, $output);
+
+ $this->deleteDuplicatesFromLogAction($output, $duplicateActions);
+
+ if ($invalidateArchives) {
+ $this->invalidateArchivesUsingActionDuplicates($allArchivesAffected, $output);
+ } else {
+ $this->printAffectedArchives($allArchivesAffected, $output);
+ }
+
+ $logActionTable = Common::prefixTable('log_action');
+ $this->writeSuccessMessage($output, array(
+ "Found and deleted $numberRemoved duplicate action entries in the $logActionTable table.",
+ "References in log_link_visit_action, log_conversion and log_conversion_item were corrected.",
+ $timer->__toString()
+ ));
+ }
+
+ private function invalidateArchivesUsingActionDuplicates($archivesAffected, OutputInterface $output)
+ {
+ $output->write("Invalidating archives affected by duplicates fixed...");
+ foreach ($archivesAffected as $archiveInfo) {
+ $this->archiveInvalidator->markArchivesAsInvalidated(
+ array($archiveInfo['idsite']), $archiveInfo['server_time'], $period = false);
+ }
+ $output->writeln("Done.");
+ }
+
+ private function printAffectedArchives($allArchivesAffected, OutputInterface $output)
+ {
+ $output->writeln("The following archives used duplicate actions and should be invalidated if you want correct reports:");
+ foreach ($allArchivesAffected as $archiveInfo) {
+ $output->writeln("\t[ idSite = {$archiveInfo['idsite']}, date = {$archiveInfo['server_time']} ]");
+ }
+ }
+
+ private function fixDuplicateActionReferences($duplicateActions, OutputInterface $output)
+ {
+ $dupeCount = count($duplicateActions);
+
+ $numberRemoved = 0;
+ $allArchivesAffected = array();
+
+ foreach ($duplicateActions as $index => $dupeInfo) {
+ $name = $dupeInfo['name'];
+ $toIdAction = $dupeInfo['idaction'];
+ $fromIdActions = $dupeInfo['duplicateIdActions'];
+
+ $numberRemoved += count($fromIdActions);
+
+ $output->writeln("<info>[$index / $dupeCount]</info> Fixing duplicates for '$name'");
+
+ $this->logger->debug(" idaction = {idaction}, duplicate idactions = {duplicateIdActions}", array(
+ 'idaction' => $toIdAction,
+ 'duplicateIdActions' => $fromIdActions
+ ));
+
+ foreach (DuplicateActionRemover::$tablesWithIdActionColumns as $table) {
+ $archivesAffected = $this->fixDuplicateActionsInTable($output, $table, $toIdAction, $fromIdActions);
+ $allArchivesAffected = array_merge($allArchivesAffected, $archivesAffected);
+ }
+ }
+
+ $allArchivesAffected = array_values(array_unique($allArchivesAffected, SORT_REGULAR));
+
+ return array($numberRemoved, $allArchivesAffected);
+ }
+
+ private function fixDuplicateActionsInTable(OutputInterface $output, $table, $toIdAction, $fromIdActions)
+ {
+ $timer = new Timer();
+
+ $archivesAffected = $this->duplicateActionRemover->getSitesAndDatesOfRowsUsingDuplicates($table, $fromIdActions);
+
+ $this->duplicateActionRemover->fixDuplicateActionsInTable($table, $toIdAction, $fromIdActions);
+
+ $output->writeln("\tFixed duplicates in " . Common::prefixTable($table) . ". <comment>" . $timer->__toString() . "</comment>.");
+
+ return $archivesAffected;
+ }
+
+ private function deleteDuplicatesFromLogAction(OutputInterface $output, $duplicateActions)
+ {
+ $logActionTable = Common::prefixTable('log_action');
+ $output->writeln("<info>Deleting duplicate actions from $logActionTable...</info>");
+
+ $idActions = array();
+ foreach ($duplicateActions as $dupeInfo) {
+ $idActions = array_merge($idActions, $dupeInfo['duplicateIdActions']);
+ }
+
+ $this->actionsAccess->delete($idActions);
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreAdminHome/Model/DuplicateActionRemover.php b/plugins/CoreAdminHome/Model/DuplicateActionRemover.php
new file mode 100644
index 0000000000..51a93bab47
--- /dev/null
+++ b/plugins/CoreAdminHome/Model/DuplicateActionRemover.php
@@ -0,0 +1,193 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\CoreAdminHome\Model;
+
+use Piwik\Common;
+use Piwik\Container\StaticContainer;
+use Piwik\DataAccess\TableMetadata;
+use Piwik\Db;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Provides methods to find duplicate actions and fix duplicate action references in tables
+ * that reference log_action rows.
+ */
+class DuplicateActionRemover
+{
+ /**
+ * The tables that contain idaction reference columns.
+ *
+ * @var string[]
+ */
+ public static $tablesWithIdActionColumns = array(
+ 'log_link_visit_action',
+ 'log_conversion',
+ 'log_conversion_item'
+ );
+
+ /**
+ * DAO used to get idaction column names in tables that reference log_action rows.
+ *
+ * @var TableMetadata
+ */
+ private $tableMetadataAccess;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * List of idaction columns in each table in $tablesWithIdActionColumns. idaction
+ * columns are table columns with the string `"idaction"` in them.
+ *
+ * @var string[]
+ */
+ private $idactionColumns;
+
+ /**
+ * Constructor.
+ *
+ * @param TableMetadata $tableMetadataAccess
+ * @param LoggerInterface $logger
+ */
+ public function __construct($tableMetadataAccess = null, $logger = null)
+ {
+ if ($tableMetadataAccess === null) {
+ $tableMetadataAccess = new TableMetadata();
+ }
+ $this->tableMetadataAccess = $tableMetadataAccess;
+
+ if ($logger === null) {
+ $logger = StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+ $this->logger = $logger;
+
+ $this->idactionColumns = $this->getIdActionTableColumnsFromMetadata();
+ }
+
+ /**
+ * Returns list of all duplicate actions in the log_action table by name and the lowest action ID.
+ * The duplicate actions are returned with each action.
+ *
+ * @return array Contains the following elements:
+ *
+ * * **name**: The action's name.
+ * * **idaction**: The action's ID.
+ * * **duplicateIdActions**: An array of duplicate action IDs.
+ */
+ public function getDuplicateIdActions()
+ {
+ $sql = "SELECT name, COUNT(*) AS count, GROUP_CONCAT(idaction ORDER BY idaction ASC SEPARATOR ',') as idactions
+ FROM " . Common::prefixTable('log_action') . "
+ GROUP BY name, hash, type HAVING count > 1";
+
+ $result = array();
+ foreach (Db::fetchAll($sql) as $row) {
+ $dupeInfo = array('name' => $row['name']);
+
+ $idActions = explode(",", $row['idactions']);
+ $dupeInfo['idaction'] = array_shift($idActions);
+ $dupeInfo['duplicateIdActions'] = $idActions;
+
+ $result[] = $dupeInfo;
+ }
+ return $result;
+ }
+
+ /**
+ * Executes one SQL statement that sets all idaction columns in a table to a single value, if the
+ * values of those columns are in the specified set (`$duplicateIdActions`).
+ *
+ * Notes:
+ *
+ * The SQL will look like:
+ *
+ * UPDATE $table SET
+ * col1 = IF((col1 IN ($duplicateIdActions)), $realIdAction, col1),
+ * col2 = IF((col2 IN ($duplicateIdActions)), $realIdAction, col2),
+ * ...
+ * WHERE col1 IN ($duplicateIdActions) OR col2 IN ($duplicateIdActions) OR ...
+ *
+ * @param string $table
+ * @param int $realIdAction The idaction to set column values to.
+ * @param int[] $duplicateIdActions The idaction values that should be changed.
+ */
+ public function fixDuplicateActionsInTable($table, $realIdAction, $duplicateIdActions)
+ {
+ $idactionColumns = array_values($this->idactionColumns[$table]);
+ $table = Common::prefixTable($table);
+
+ $inFromIdsExpression = $this->getInFromIdsExpression($duplicateIdActions);
+ $setExpression = "%1\$s = IF(($inFromIdsExpression), $realIdAction, %1\$s)";
+
+ $sql = "UPDATE $table SET\n";
+ foreach ($idactionColumns as $index => $column) {
+ if ($index != 0) {
+ $sql .= ",\n";
+ }
+ $sql .= sprintf($setExpression, $column);
+ }
+ $sql .= $this->getWhereToGetRowsUsingDuplicateActions($idactionColumns, $duplicateIdActions);
+
+ Db::query($sql);
+ }
+
+ /**
+ * Returns the server time and idsite of rows in a log table that reference at least one action
+ * in a set.
+ *
+ * @param string $table
+ * @param int[] $duplicateIdActions
+ * @return array with two elements **idsite** and **server_time**. idsite is the site ID and server_time
+ * is the date of the log.
+ */
+ public function getSitesAndDatesOfRowsUsingDuplicates($table, $duplicateIdActions)
+ {
+ $idactionColumns = array_values($this->idactionColumns[$table]);
+ $table = Common::prefixTable($table);
+
+ $sql = "SELECT idsite, DATE(server_time) as server_time FROM $table ";
+ $sql .= $this->getWhereToGetRowsUsingDuplicateActions($idactionColumns, $duplicateIdActions);
+ return Db::fetchAll($sql);
+ }
+
+ private function getIdActionTableColumnsFromMetadata()
+ {
+ $result = array();
+ foreach (self::$tablesWithIdActionColumns as $table) {
+ $columns = $this->tableMetadataAccess->getIdActionColumnNames(Common::prefixTable($table));
+
+ $this->logger->debug("Found following idactions in {table}: {columns}", array(
+ 'table' => $table,
+ 'columns' => implode(',', $columns)
+ ));
+
+ $result[$table] = $columns;
+ }
+ return $result;
+ }
+
+ private function getWhereToGetRowsUsingDuplicateActions($idactionColumns, $fromIdActions)
+ {
+ $sql = "WHERE ";
+ foreach ($idactionColumns as $index => $column) {
+ if ($index != 0) {
+ $sql .= "OR ";
+ }
+
+ $sql .= sprintf($this->getInFromIdsExpression($fromIdActions), $column) . " ";
+ }
+ return $sql;
+ }
+
+ private function getInFromIdsExpression($fromIdActions)
+ {
+ return "%1\$s IN (" . implode(',', $fromIdActions) . ")";
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/Fixture/DuplicateActions.php b/plugins/CoreAdminHome/tests/Fixture/DuplicateActions.php
new file mode 100644
index 0000000000..7d98a57670
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Fixture/DuplicateActions.php
@@ -0,0 +1,171 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\CoreAdminHome\tests\Fixture;
+
+use Piwik\Common;
+use Piwik\Db;
+use Piwik\Tests\Framework\Fixture;
+
+/**
+ * Fixture that adds log table rows that use duplicate actions.
+ */
+class DuplicateActions extends Fixture
+{
+ const DUMMY_IDVISITOR = 'c1d2a36653fd88e2';
+
+ private static $dataToInsert = array(
+ 'log_action' => array(
+ array('name' => 'action1', 'type' => 1),
+ array('name' => 'action1', 'type' => 1),
+ array('name' => 'action1', 'type' => 1),
+
+ array('name' => 'action2', 'type' => 2),
+ array('name' => 'ACTION2', 'type' => 1),
+ array('name' => 'action4', 'type' => 3),
+ array('name' => 'ACTION2', 'type' => 1),
+ array('name' => 'action5', 'type' => 2),
+
+ array('name' => 'action2', 'type' => 2),
+ array('name' => 'action4', 'type' => 3),
+ array('name' => 'ACTION2', 'type' => 1),
+ array('name' => 'action4', 'type' => 3),
+ ),
+ 'log_link_visit_action' => array(
+ array(
+ 'idsite' => 1,
+ 'idvisitor' => self::DUMMY_IDVISITOR,
+ 'idvisit' => 1,
+ 'server_time' => '2012-01-01 00:00:00',
+ 'time_spent_ref_action' => 100,
+ 'idaction_url_ref' => 1,
+ 'idaction_name_ref' => 2,
+ 'idaction_name' => 3,
+ 'idaction_url' => 4,
+ 'idaction_event_action' => 5,
+ 'idaction_event_category' => 6,
+ 'idaction_content_interaction' => 7,
+ 'idaction_content_name' => 8,
+ 'idaction_content_piece' => 9,
+ 'idaction_content_target' => 10,
+ ),
+ array(
+ 'idsite' => 2,
+ 'idvisitor' => self::DUMMY_IDVISITOR,
+ 'idvisit' => 2,
+ 'server_time' => '2013-01-01 00:00:00',
+ 'time_spent_ref_action' => 120,
+ 'idaction_url_ref' => 2,
+ 'idaction_name_ref' => 3,
+ 'idaction_name' => 5,
+ 'idaction_url' => 7,
+ 'idaction_event_action' => 9,
+ 'idaction_event_category' => 10,
+ 'idaction_content_interaction' => 11,
+ 'idaction_content_name' => 11,
+ 'idaction_content_piece' => 12,
+ 'idaction_content_target' => 12,
+ ),
+ ),
+ 'log_conversion' => array(
+ array(
+ 'idvisit' => 1,
+ 'idsite' => 1,
+ 'idvisitor' => self::DUMMY_IDVISITOR,
+ 'server_time' => '2012-02-01 00:00:00',
+ 'idgoal' => 1,
+ 'buster' => 1,
+ 'url' => 'http://example.com/',
+ 'location_country' => 'nz',
+ 'visitor_count_visits' => 1,
+ 'visitor_returning' => 1,
+ 'visitor_days_since_order' => 1,
+ 'visitor_days_since_first' => 1,
+ 'idaction_url' => 4,
+ ),
+
+ array(
+ 'idvisit' => 2,
+ 'idsite' => 2,
+ 'idvisitor' => self::DUMMY_IDVISITOR,
+ 'server_time' => '2012-03-01 00:00:00',
+ 'idgoal' => 2,
+ 'buster' => 2,
+ 'url' => 'http://example.com/',
+ 'location_country' => 'nz',
+ 'visitor_count_visits' => 1,
+ 'visitor_returning' => 1,
+ 'visitor_days_since_order' => 1,
+ 'visitor_days_since_first' => 1,
+ 'idaction_url' => 7,
+ )
+ ),
+ 'log_conversion_item' => array(
+ array(
+ 'idsite' => 1,
+ 'idvisitor' => self::DUMMY_IDVISITOR,
+ 'server_time' => '2012-02-01 00:00:00',
+ 'idvisit' => 1,
+ 'idorder' => 1,
+ 'price' => 10,
+ 'quantity' => 2,
+ 'deleted' => 0,
+ 'idaction_sku' => 1,
+ 'idaction_name' => 2,
+ 'idaction_category' => 3,
+ 'idaction_category2' => 4,
+ 'idaction_category3' => 5,
+ 'idaction_category4' => 6,
+ 'idaction_category5' => 7,
+ ),
+ array(
+ 'idsite' => 2,
+ 'idvisitor' => self::DUMMY_IDVISITOR,
+ 'server_time' => '2012-01-09 00:00:00',
+ 'idvisit' => 2,
+ 'idorder' => 2,
+ 'price' => 10,
+ 'quantity' => 1,
+ 'deleted' => 1,
+ 'idaction_sku' => 2,
+ 'idaction_name' => 3,
+ 'idaction_category' => 5,
+ 'idaction_category2' => 7,
+ 'idaction_category3' => 8,
+ 'idaction_category4' => 9,
+ 'idaction_category5' => 10,
+ )
+ )
+ );
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ foreach (self::$dataToInsert as $table => $rows) {
+ self::insertRowData($table, $rows);
+ }
+ }
+
+ private static function insertRowData($unprefixedTable, $rows)
+ {
+ $table = Common::prefixTable($unprefixedTable);
+ foreach ($rows as $row) {
+ if ($unprefixedTable == 'log_action') {
+ $row['hash'] = crc32($row['name']);
+ }
+
+ if (isset($row['idvisitor'])) {
+ $row['idvisitor'] = pack("H*", $row['idvisitor']);
+ }
+
+ $placeholders = array_map(function () { return "?"; }, $row);
+ $sql = "INSERT INTO $table (" . implode(',', array_keys($row)) . ") VALUES (" . implode(',', $placeholders) . ")";
+ Db::query($sql, array_values($row));
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/Integration/FixDuplicateActionsTest.php b/plugins/CoreAdminHome/tests/Integration/FixDuplicateActionsTest.php
new file mode 100644
index 0000000000..ca56f3b026
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Integration/FixDuplicateActionsTest.php
@@ -0,0 +1,175 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\CoreAdminHome\tests\Integration;
+
+use Piwik\Common;
+use Piwik\Console;
+use Piwik\Db;
+use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Plugins\CoreAdminHome\tests\Fixture\DuplicateActions;
+use Piwik\Plugins\QueuedTracking\tests\Framework\TestCase\IntegrationTestCase;
+use Symfony\Component\Console\Tester\ApplicationTester;
+
+/**
+ * @group Core
+ */
+class FixDuplicateActionsTest extends IntegrationTestCase
+{
+ /**
+ * @var DuplicateActions
+ */
+ public static $fixture = null;
+
+ /**
+ * @var ApplicationTester
+ */
+ protected $applicationTester = null;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $application = new Console();
+ $application->setAutoExit(false);
+
+ $this->applicationTester = new ApplicationTester($application);
+ }
+
+ public function test_FixDuplicateLogActions_CorrectlyRemovesDuplicates_AndFixesReferencesInOtherTables()
+ {
+ $result = $this->applicationTester->run(array(
+ 'command' => 'core:fix-duplicate-log-actions',
+ '--invalidate-archives' => 0,
+ '-vvv' => false
+ ));
+
+ $this->assertEquals(0, $result, "Command failed: " . $this->applicationTester->getDisplay());
+
+ $this->assertDuplicateActionsRemovedFromLogActionTable();
+ $this->assertDuplicatesFixedInLogLinkVisitActionTable();
+ $this->assertDuplicatesFixedInLogConversionTable();
+ $this->assertDuplicatesFixedInLogConversionItemTable();
+
+ $this->assertContains("Found and deleted 7 duplicate action entries", $this->applicationTester->getDisplay());
+
+ $expectedAffectedArchives = array(
+ array('idsite' => '1', 'server_time' => '2012-01-01'),
+ array('idsite' => '2', 'server_time' => '2013-01-01'),
+ array('idsite' => '1', 'server_time' => '2012-02-01'),
+ array('idsite' => '2', 'server_time' => '2012-01-09'),
+ array('idsite' => '2', 'server_time' => '2012-03-01'),
+ );
+ foreach ($expectedAffectedArchives as $archive) {
+ $this->assertContains("[ idSite = {$archive['idsite']}, date = {$archive['server_time']} ]", $this->applicationTester->getDisplay());
+ }
+ }
+
+ private function assertDuplicateActionsRemovedFromLogActionTable()
+ {
+ $actions = Db::fetchAll("SELECT idaction, name FROM " . Common::prefixTable('log_action'));
+ $expectedActions = array(
+ array('idaction' => 1, 'name' => 'action1'),
+ array('idaction' => 4, 'name' => 'action2'),
+ array('idaction' => 5, 'name' => 'ACTION2'),
+ array('idaction' => 6, 'name' => 'action4'),
+ array('idaction' => 8, 'name' => 'action5'),
+ );
+ $this->assertEquals($expectedActions, $actions);
+ }
+
+ private function assertDuplicatesFixedInLogLinkVisitActionTable()
+ {
+ $columns = array(
+ 'idaction_url_ref',
+ 'idaction_name_ref',
+ 'idaction_name',
+ 'idaction_url',
+ 'idaction_event_action',
+ 'idaction_event_category',
+ 'idaction_content_interaction',
+ 'idaction_content_name',
+ 'idaction_content_piece',
+ 'idaction_content_target'
+ );
+ $rows = Db::fetchAll("SELECT " . implode(',', $columns) . " FROM " . Common::prefixTable('log_link_visit_action'));
+ $expectedRows = array(
+ array(
+ 'idaction_url_ref' => '1',
+ 'idaction_name_ref' => '1',
+ 'idaction_name' => '1',
+ 'idaction_url' => '4',
+ 'idaction_event_action' => '5',
+ 'idaction_event_category' => '6',
+ 'idaction_content_interaction' => '5',
+ 'idaction_content_name' => '8',
+ 'idaction_content_piece' => '4',
+ 'idaction_content_target' => '6'
+ ),
+ array(
+ 'idaction_url_ref' => '1',
+ 'idaction_name_ref' => '1',
+ 'idaction_name' => '5',
+ 'idaction_url' => '5',
+ 'idaction_event_action' => '4',
+ 'idaction_event_category' => '6',
+ 'idaction_content_interaction' => '5',
+ 'idaction_content_name' => '5',
+ 'idaction_content_piece' => '6',
+ 'idaction_content_target' => '6'
+ )
+ );
+ $this->assertEquals($expectedRows, $rows);
+ }
+
+ private function assertDuplicatesFixedInLogConversionTable()
+ {
+ $rows = Db::fetchAll("SELECT idaction_url FROM " . Common::prefixTable('log_conversion'));
+ $expectedRows = array(
+ array('idaction_url' => 4),
+ array('idaction_url' => 5)
+ );
+ $this->assertEquals($expectedRows, $rows);
+ }
+
+ private function assertDuplicatesFixedInLogConversionItemTable()
+ {
+ $columns = array(
+ 'idaction_sku',
+ 'idaction_name',
+ 'idaction_category',
+ 'idaction_category2',
+ 'idaction_category3',
+ 'idaction_category4',
+ 'idaction_category5'
+ );
+ $rows = Db::fetchAll("SELECT " . implode(',', $columns) . " FROM " . Common::prefixTable('log_conversion_item'));
+ $expectedRows = array(
+ array(
+ 'idaction_sku' => '1',
+ 'idaction_name' => '1',
+ 'idaction_category' => '1',
+ 'idaction_category2' => '4',
+ 'idaction_category3' => '5',
+ 'idaction_category4' => '6',
+ 'idaction_category5' => '5'
+ ),
+ array(
+ 'idaction_sku' => '1',
+ 'idaction_name' => '1',
+ 'idaction_category' => '5',
+ 'idaction_category2' => '5',
+ 'idaction_category3' => '8',
+ 'idaction_category4' => '4',
+ 'idaction_category5' => '6'
+ )
+ );
+ $this->assertEquals($expectedRows, $rows);
+ }
+}
+
+FixDuplicateActionsTest::$fixture = new DuplicateActions(); \ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/Integration/Model/DuplicateActionRemoverTest.php b/plugins/CoreAdminHome/tests/Integration/Model/DuplicateActionRemoverTest.php
new file mode 100644
index 0000000000..a898ef1616
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Integration/Model/DuplicateActionRemoverTest.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugins\CoreAdminHome\tests\Integration\Model;
+
+use Piwik\Common;
+use Piwik\Db;
+use Piwik\Plugins\CoreAdminHome\Model\DuplicateActionRemover;
+use Piwik\Plugins\CoreAdminHome\tests\Fixture\DuplicateActions;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group Core
+ */
+class DuplicateActionRemoverTest extends IntegrationTestCase
+{
+ /**
+ * @var DuplicateActions
+ */
+ public static $fixture = null;
+
+ /**
+ * @var DuplicateActionRemover
+ */
+ private $duplicateActionRemover;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $this->duplicateActionRemover = new DuplicateActionRemover();
+ }
+
+ public function test_getDuplicateIdActions_ReturnsDuplicateIdActions_AndTreatsLowestIdActionAsOriginal()
+ {
+ $expectedResult = array(
+ array('name' => 'action1', 'idaction' => 1, 'duplicateIdActions' => array(2, 3)),
+ array('name' => 'ACTION2', 'idaction' => 5, 'duplicateIdActions' => array(7, 11)),
+ array('name' => 'action2', 'idaction' => 4, 'duplicateIdActions' => array(9)),
+ array('name' => 'action4', 'idaction' => 6, 'duplicateIdActions' => array(10, 12)),
+ );
+ $actualResult = $this->duplicateActionRemover->getDuplicateIdActions();
+ $this->assertEquals($expectedResult, $actualResult);
+ }
+
+ public function test_fixDuplicateActionsInTable_CorrectlyUpdatesIdActionColumns_InSpecifiedTable()
+ {
+ $this->duplicateActionRemover->fixDuplicateActionsInTable('log_conversion_item', 5, array(3, 6, 7, 10));
+
+ $columns = array('idaction_sku', 'idaction_name', 'idaction_category', 'idaction_category2',
+ 'idaction_category3', 'idaction_category4', 'idaction_category5');
+
+ $expectedResult = array(
+ array(
+ 'idaction_sku' => '1',
+ 'idaction_name' => '2',
+ 'idaction_category' => '5',
+ 'idaction_category2' => '4',
+ 'idaction_category3' => '5',
+ 'idaction_category4' => '5',
+ 'idaction_category5' => '5'
+ ),
+ array(
+ 'idaction_sku' => '2',
+ 'idaction_name' => '5',
+ 'idaction_category' => '5',
+ 'idaction_category2' => '5',
+ 'idaction_category3' => '8',
+ 'idaction_category4' => '9',
+ 'idaction_category5' => '5'
+ ),
+ );
+ $actualResult = Db::fetchAll("SELECT " . implode(", ", $columns) . " FROM " . Common::prefixTable('log_conversion_item'));
+ $this->assertEquals($expectedResult, $actualResult);
+ }
+
+ public function test_getSitesAndDatesOfRowsUsingDuplicates_ReturnsTheServerTimeAndIdSite_OfRowsUsingSpecifiedActionIds()
+ {
+ $row = array(
+ 'idsite' => 3,
+ 'idvisitor' => pack("H*", DuplicateActions::DUMMY_IDVISITOR),
+ 'server_time' => '2012-02-13 00:00:00',
+ 'idvisit' => 5,
+ 'idorder' => 6,
+ 'price' => 15,
+ 'quantity' => 21,
+ 'deleted' => 1,
+ 'idaction_sku' => 3,
+ 'idaction_name' => 3,
+ 'idaction_category' => 12,
+ 'idaction_category2' => 3,
+ 'idaction_category3' => 3,
+ 'idaction_category4' => 3,
+ 'idaction_category5' => 3,
+ );
+ Db::query("INSERT INTO " . Common::prefixTable('log_conversion_item') . " (" . implode(", ", array_keys($row))
+ . ") VALUES ('" . implode("', '", array_values($row)) . "')");
+
+ $expectedResult = array(
+ array('idsite' => 1, 'server_time' => '2012-02-01'),
+ array('idsite' => 3, 'server_time' => '2012-02-13')
+ );
+ $actualResult = $this->duplicateActionRemover->getSitesAndDatesOfRowsUsingDuplicates('log_conversion_item', array(4, 6, 12));
+ $this->assertEquals($expectedResult, $actualResult);
+ }
+}
+
+DuplicateActionRemoverTest::$fixture = new DuplicateActions(); \ No newline at end of file