isEnabledByName('bx_analytics')) $this->markTestSkipped('bx_analytics is not installed'); $this->_oModule = BxDolModule::getInstance('bx_analytics'); $this->_oModule->getCollector(); // loads the collector classes $this->_oModule->getFilter([]); // the module's alert handler, with a collector for the test's request and time $aModule = BxDolModuleQuery::getInstance()->getModuleByName('bx_analytics'); $this->_oAlertModule = new class($aModule) extends BxAnalyticsModule { public $oCollector = null; public function getCollector() { return $this->oCollector ? $this->oCollector : parent::getCollector(); } }; $this->_cleanup(); foreach (['bx_analytics_track_actions' => 'on', 'bx_analytics_tracking' => 'on', 'bx_analytics_platforms' => 'core-web,next-web,rn-ios,rn-android,api,core', 'bx_analytics_respect_dnt' => 'on', 'bx_analytics_link_members' => 'on', 'bx_analytics_identity' => 'cookieless', 'bx_analytics_exclude_ips' => '', 'bx_analytics_exclude_levels' => '', 'bx_analytics_exclude_paths' => ''] as $sName => $sValue) $this->_setOption($sName, $sValue); } protected function tearDown(): void { if ($this->_oModule) { $this->_cleanup(); foreach ($this->_aOptions as $sName => $sValue) setParam($sName, $sValue); } parent::tearDown(); } public function testGoalAndFunnelCountAVisitThatAddedAPost() { $oDb = $this->_oModule->_oDb; $aGoal = ['type' => 'event', 'pattern' => 'content_add@bx_posts']; $iGoal = $oDb->saveGoal(0, ['name' => 'Test 2001 new post'] + $aGoal); $iFunnel = $oDb->saveFunnel(0, 'Test 2001 posting', [ ['type' => 'page', 'pattern' => '/page/create-post'], $aGoal, ]); $this->_aGoals = [$iGoal]; $this->_aFunnels = [$iFunnel]; // A adds a post, B adds a photo, C only looks at the form $aVisits = [['93.184.216.61', 'bx_posts'], ['93.184.216.62', 'bx_photos'], ['93.184.216.63', '']]; foreach ($aVisits as $iVisit => list($sIp, $sUnit)) { $aServer = $this->_server($sIp); $iNow = self::NOW + $iVisit * 100; $this->_collect($aServer, ['platform' => 'core-web', 'hits' => [['type' => 'pageview', 'url' => 'https://una.test/page/create-post']]], $iNow); if ($sUnit !== '') $this->_alert($aServer, $iNow + 30, $sUnit, 'added', 70 + $iVisit, self::PROFILE + $iVisit); } $aSessions = $this->_sessions(); $this->assertCount(3, $aSessions); $aEvents = BxDolDb::getInstance()->getAll("SELECT `session_id`, `name`, `module`, `object_id` FROM `bx_analytics_events` WHERE `type` = 'una' AND `ts` < :to ORDER BY `id`", ['to' => self::NOW + 86400]); $this->assertEquals([ ['session_id' => $aSessions[0]['id'], 'name' => 'content_add', 'module' => 'bx_posts', 'object_id' => 70], ['session_id' => $aSessions[1]['id'], 'name' => 'content_add', 'module' => 'bx_photos', 'object_id' => 71], ], $aEvents, 'a new post is the site action content_add of the module, in the visit'); $oReports = $this->_oModule->getReports(); $oFilter = $this->_filter(); $aGoals = array_column($oReports->getGoals($oFilter)['goals'], null, 'id'); $this->assertSame(1, $aGoals[$iGoal]['conversions']); $this->assertEquals(33.33, $aGoals[$iGoal]['rate']); $aFilter = $oFilter->get(); $this->assertSame(2, (int)$oDb->getGoalTotals($aFilter, ['type' => 'event', 'pattern' => 'content_add'])['sessions'], 'without a module, new content of any module'); $this->assertSame(0, (int)$oDb->getGoalTotals($aFilter, ['type' => 'event', 'pattern' => 'added@bx_posts'])['sessions'], 'the alert\'s name is not the action\'s'); $aFunnel = $oReports->getFunnels($oFilter, ['funnel' => $iFunnel]); $this->assertSame([3, 1], array_column($aFunnel['steps'], 'sessions')); } public function testHintExamplesAreRecordedActions() { // one visit that does every site action the alert handler records $aServer = $this->_server('93.184.216.64'); $this->_collect($aServer, ['platform' => 'core-web', 'hits' => [['type' => 'pageview', 'url' => 'https://una.test/page/view-post?id=70']]], self::NOW); $aAlerts = [ // the test account has no profile, so it signs in as the guest the visit started as ['account', 'add', self::ACCOUNT, 0, []], ['account', 'login', self::ACCOUNT, 0, []], ['profile', 'add', self::PROFILE, self::PROFILE, ['module' => 'bx_persons']], ['bx_posts', 'added', 70, self::PROFILE, []], ['comment', 'added', 1, self::PROFILE, ['object_system' => 'bx_posts', 'object_id' => 70]], ['vote', 'do', 1, self::PROFILE, ['object_system' => 'bx_posts_reactions', 'object_id' => 70, 'reaction' => 'like']], ['system', 'connection_added', 5, self::PROFILE, ['object_name' => 'sys_profiles_friends', 'content' => 6]], ]; foreach ($aAlerts as $i => list($sUnit, $sAction, $iObject, $iSender, $aExtras)) $this->_alert($aServer, self::NOW + 10 + $i, $sUnit, $sAction, $iObject, $iSender, $aExtras); // the same post page as the other clients record it: the classic site without // permalinks, NEO on the web and the apps (the UNA page path, see NEO's toPagePath) $aPostPages = [ ['93.184.216.65', self::UA_SAFARI, ['platform' => 'core-web', 'url' => 'https://una.test/page.php?i=view-post&id=70']], ['93.184.216.66', self::UA_SAFARI, ['platform' => 'next-web', 'url' => 'https://neo.test/view-post?id=70']], ['93.184.216.67', self::UA_APP, ['platform' => 'rn-ios', 'app' => 'com.neo.so', 'url' => '/view-post?id=70']], ['93.184.216.68', self::UA_APP, ['platform' => 'rn-android', 'app' => 'com.neo.so', 'url' => '/view-post?id=70']], ]; foreach ($aPostPages as $i => list($sIp, $sUserAgent, $aHit)) { $sUrl = $aHit['url']; unset($aHit['url']); $this->_collect($this->_server($sIp, $sUserAgent), $aHit + ['hits' => [['type' => 'pageview', 'url' => $sUrl]]], self::NOW + 100 + $i); } $aSessions = $this->_sessions(); $this->assertCount(1 + count($aPostPages), $aSessions); $this->assertEquals(['core-web', 'core-web', 'next-web', 'rn-ios', 'rn-android'], array_column($aSessions, 'platform')); $aRecorded = BxDolDb::getInstance()->getColumn("SELECT DISTINCT `name` FROM `bx_analytics_events` WHERE `type` = 'una' AND `session_id` = :id", ['id' => $aSessions[0]['id']]); $this->assertCount(count($aAlerts), $aRecorded, 'every alert is a site action of the visit'); // every action has a title of its own in the reports $aEn = $this->_strings(BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/install/langs/en.xml'); $aRu = $this->_strings(BX_DIRECTORY_PATH_MODULES . 'boonex/russian/data/langs/bx_analytics/ru.xml'); foreach ($aRecorded as $sName) { $this->assertArrayHasKey('_bx_analytics_action_' . $sName, $aEn, $sName); $this->assertArrayHasKey('_bx_analytics_action_' . $sName, $aRu, $sName); } $oDb = $this->_oModule->_oDb; $aFilter = $this->_filter()->get(); foreach (self::HINTS as $sKey) { $this->assertArrayHasKey($sKey, $aEn); $this->assertArrayHasKey($sKey, $aRu); // Russian prose is Cyrillic, so its Latin words are the examples $aExamples = $this->_examples($aRu[$sKey], '/(?assertNotEmpty($aExamples['page'], $sKey . ': a page example'); $this->assertNotEmpty(array_filter($aExamples['event'], function ($s) { return strpos($s, '@') !== false; }), $sKey . ': an action of one module'); // the English hint shows the same examples $aEnExamples = $this->_examples($aEn[$sKey], '/(?assertSame($aExamples['page'], $aEnExamples['page'], $sKey); $this->assertSame(array_values(array_filter($aExamples['event'], function ($s) { return strpos($s, '@') !== false; })), array_values(array_diff($aEnExamples['event'], [self::PLACEHOLDER])), $sKey); foreach ($aExamples['event'] as $sExample) $this->assertMatchesRegularExpression('/(?assertContains(explode('@', $sPattern)[0], $aRecorded, $sKey . ': ' . $sPattern . ' names an action the module records'); // and each of them, as a goal, counts the visit that did it; a page example // counts the post page on every client foreach ($aExamples as $sType => $aPatterns) foreach ($aPatterns as $sPattern) $this->assertSame($sType == 'page' ? count($aSessions) : 1, (int)$oDb->getGoalTotals($aFilter, ['type' => $sType, 'pattern' => $sPattern])['sessions'], $sKey . ': ' . $sPattern . ' matches'); } } /** * Page paths and action names a hint gives as examples. */ protected function _examples($sText, $sActionRegExp) { $aPages = []; if (preg_match_all('#(? $aPages, 'event' => $aEvents]; } protected function _strings($sFile) { $this->assertFileExists($sFile); $aResult = []; foreach (simplexml_load_file($sFile)->xpath('//string') as $oString) $aResult[(string)$oString['name']] = (string)$oString; return $aResult; } protected function _alert($aServer, $iNow, $sUnit, $sAction, $iObject, $iSender, $aExtras = []) { $this->_oAlertModule->oCollector = new BxAnalyticsCollector($this->_oModule, new BxAnalyticsRequest($aServer), $iNow); $this->_oAlertModule->onAlert(new BxDolAlerts($sUnit, $sAction, $iObject, $iSender, $aExtras)); $this->_oAlertModule->oCollector = null; } protected function _collect($aServer, $aPayload, $iNow) { $oCollector = new BxAnalyticsCollector($this->_oModule, new BxAnalyticsRequest($aServer), $iNow); return $oCollector->collect($aPayload, ['platform' => 'api']); } protected function _filter() { return new BxAnalyticsFilter($this->_oModule, ['range' => 'custom', 'from' => '2001-09-09', 'to' => '2001-09-09'], self::NOW + 86400); } protected function _server($sIp, $sUserAgent = self::UA_SAFARI) { return ['REMOTE_ADDR' => $sIp, 'HTTP_USER_AGENT' => $sUserAgent, 'REQUEST_METHOD' => 'POST']; } protected function _sessions() { return BxDolDb::getInstance()->getAll("SELECT * FROM `bx_analytics_sessions` WHERE `started` < :to ORDER BY `id`", ['to' => self::NOW + 30 * 86400]); } protected function _setOption($sName, $sValue) { if (!array_key_exists($sName, $this->_aOptions)) $this->_aOptions[$sName] = getParam($sName); setParam($sName, $sValue); } protected function _cleanup() { $oDb = BxDolDb::getInstance(); $iTo = self::NOW + 30 * 86400; $oDb->query("DELETE FROM `bx_analytics_events` WHERE `ts` < :to", ['to' => $iTo]); $oDb->query("DELETE FROM `bx_analytics_sessions` WHERE `started` < :to", ['to' => $iTo]); $oDb->query("DELETE FROM `bx_analytics_salts` WHERE `day` < '2002-01-01'"); $oDb->query("DELETE FROM `bx_analytics_limits` WHERE `bucket` < 1009843200"); foreach ($this->_aGoals as $iId) $this->_oModule->_oDb->deleteGoal($iId); foreach ($this->_aFunnels as $iId) $this->_oModule->_oDb->deleteFunnel($iId); $this->_aGoals = $this->_aFunnels = []; $this->_oModule->cleanReportCache(); // reports of the test data cached by the run before } }