AnalyticsExploreTest.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. <?php
  2. /**
  3. * Explore: the Query report (any dimension by any metric), its CSV download, who may use
  4. * them, and custom events from server code. Test hits are dated in September 2001, far from
  5. * real traffic, and deleted afterwards.
  6. */
  7. class AnalyticsExploreTest extends BxDolIntegrationTestCase
  8. {
  9. // 2001-09-09 01:46:40 UTC
  10. const NOW = 1000000000;
  11. const UA_SAFARI = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15';
  12. const EVENT = 'phpunit_explore_track';
  13. protected $_oModule;
  14. protected $_aOptions = [];
  15. protected $_aMatrix = [];
  16. protected $_aDay = ['range' => 'custom', 'from' => '2001-09-09', 'to' => '2001-09-09'];
  17. protected function setUp(): void
  18. {
  19. parent::setUp();
  20. if (!BxDolModuleQuery::getInstance()->isEnabledByName('bx_analytics'))
  21. $this->markTestSkipped('bx_analytics is not installed');
  22. $this->_oModule = BxDolModule::getInstance('bx_analytics');
  23. $this->_oModule->getCollector(); // loads the collector classes
  24. $this->_oModule->getFilter([]);
  25. bx_import('Csv', $this->_oModule->_aModule);
  26. $this->_cleanup();
  27. 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' => '', 'bx_analytics_geo_source' => 'headers'] as $sName => $sValue)
  28. $this->_setOption($sName, $sValue);
  29. }
  30. protected function tearDown(): void
  31. {
  32. if ($this->_oModule) {
  33. $this->_cleanup();
  34. foreach ($this->_aOptions as $sName => $sValue)
  35. setParam($sName, $sValue);
  36. $this->_restoreMatrix();
  37. }
  38. $GLOBALS['glForceCurrentProfileId'] = 0;
  39. parent::tearDown();
  40. }
  41. public function testEveryDimensionAndMetricIsSorted()
  42. {
  43. $this->_seed();
  44. $oReports = $this->_oModule->getReports();
  45. $aTotals = $this->_oModule->_oDb->getTotals($this->_filter($this->_aDay)->get());
  46. $this->assertSame(7, (int)$aTotals['visitors']);
  47. $aDimensions = $this->_oModule->_oDb->getDimensions();
  48. $this->assertCount(24, $aDimensions);
  49. $this->assertSame('path', key($aDimensions), 'pages come first: the default');
  50. foreach ($aDimensions as $sDimension => $sKind) {
  51. $aMetrics = $this->_oModule->_oDb->getBreakdownMetrics($sDimension);
  52. $this->assertSame($sKind == 'event' ? ['visitors', 'pageviews', 'engaged'] : ['visitors', 'sessions', 'pageviews', 'pages_per_session', 'bounce_rate', 'duration'], $aMetrics);
  53. $this->assertNotSame('_bx_analytics_dim_' . $sDimension, _t('_bx_analytics_dim_' . $sDimension), 'a title for ' . $sDimension);
  54. foreach ($aMetrics as $sMetric)
  55. foreach (['desc', 'asc'] as $sDir) {
  56. $sCase = $sDimension . ' by ' . $sMetric . ' ' . $sDir;
  57. $a = $oReports->getQuery($this->_filter($this->_aDay), ['q_dim' => $sDimension, 'q_metric' => $sMetric, 'q_dir' => $sDir]);
  58. $this->assertSame([$sDimension, $sKind, $sMetric, $sDir], [$a['dimension'], $a['kind'], $a['metric'], $a['dir']], $sCase);
  59. $this->assertNotEmpty($a['rows'], $sCase);
  60. $this->assertSame(count($a['rows']), $a['total'], $sCase);
  61. $aValues = array_map(function ($aRow) use ($sMetric) { return $aRow['values'][$sMetric]; }, $a['rows']);
  62. $aSorted = $aValues;
  63. $sDir == 'asc' ? sort($aSorted) : rsort($aSorted);
  64. $this->assertEquals($aSorted, $aValues, $sCase);
  65. $this->assertSame([$sDir == 'asc' ? 'ascending' : 'descending'], array_values(array_filter(array_column($a['columns'], 'sort'))), $sCase . ': the sorted column is marked');
  66. if ($sKind == 'session')
  67. $this->assertGreaterThanOrEqual(7, array_sum(array_map(function ($aRow) { return $aRow['values']['visitors']; }, $a['rows'])), $sCase . ': every visitor is in some row');
  68. }
  69. }
  70. }
  71. public function testQueryRowsAndChart()
  72. {
  73. $this->_seed();
  74. $oReports = $this->_oModule->getReports();
  75. $oFilter = $this->_filter($this->_aDay);
  76. $a = $oReports->getQuery($oFilter, []);
  77. $this->assertSame(['query', 'path', 'visitors', 'desc'], [$a['name'], $a['dimension'], $a['metric'], $a['dir']]);
  78. $this->assertSame([], $a['tabs']);
  79. $this->assertSame(7, $a['total']);
  80. $this->assertSame(['/a', '/c'], array_slice(array_column($a['rows'], 'label'), 0, 2), '3 visitors on /a, 2 on /c');
  81. $this->assertSame(['dim' => 'path', 'value' => '/a'], $a['rows'][0]['filter']);
  82. $this->assertSame(3, $a['rows'][0]['values']['entries']);
  83. $this->assertEquals(10, $a['rows'][0]['values']['engaged'], 'average engaged seconds of the views that reported it');
  84. // the chart: the first five values, every bucket of the period
  85. $aBuckets = $oFilter->get()['buckets'];
  86. $this->assertSame('series', $a['chart']['kind']);
  87. $this->assertCount(5, $a['chart']['series']);
  88. $this->assertSame(array_slice(array_column($a['rows'], 'label'), 0, 5), array_column($a['chart']['series'], 'label'));
  89. foreach ($a['chart']['series'] as $aSeries)
  90. $this->assertSame($aBuckets, array_column($aSeries['points'], 't'));
  91. // counts over the buckets add up to the row, for pages too (events have a `value`
  92. // column, which the grouping once took for the value asked)
  93. $a = $oReports->getQuery($oFilter, ['q_dim' => 'path', 'q_metric' => 'pageviews']);
  94. foreach ($a['chart']['series'] as $i => $aSeries)
  95. $this->assertSame($a['rows'][$i]['values']['pageviews'], array_sum(array_column($aSeries['points'], 'v')), $a['rows'][$i]['label']);
  96. $a = $oReports->getQuery($oFilter, ['q_dim' => 'country', 'q_metric' => 'pageviews']);
  97. $this->assertSame(['GB', 'US', 'FR', 'DE'], array_column(array_column($a['rows'], 'filter'), 'value'));
  98. foreach ($a['chart']['series'] as $i => $aSeries)
  99. $this->assertSame($a['rows'][$i]['values']['pageviews'], array_sum(array_column($aSeries['points'], 'v')));
  100. // a rate is a gap where there were no visits, not a zero
  101. $a = $oReports->getQuery($oFilter, ['q_dim' => 'platform', 'q_metric' => 'bounce_rate', 'q_dir' => 'asc']);
  102. $this->assertContains(null, array_column($a['chart']['series'][0]['points'], 'v'));
  103. $this->assertSame('rn-ios', $a['rows'][0]['filter']['value'], 'no bounces on iOS');
  104. $this->assertSame('platform', $a['rows'][0]['filter']['dim'], 'the script sets the platform parameter for it');
  105. // filters apply
  106. $a = $oReports->getQuery($this->_filter($this->_aDay + ['f' => ['country' => 'GB']]), ['q_dim' => 'path']);
  107. $this->assertEqualsCanonicalizing(['/a', '/b', '/d'], array_column($a['rows'], 'label'));
  108. $a = $oReports->getQuery($this->_filter($this->_aDay + ['platform' => 'rn-ios']), ['q_dim' => 'country']);
  109. $this->assertSame(['US'], array_column(array_column($a['rows'], 'filter'), 'value'));
  110. // the report through the module, as blocks and the API ask for it
  111. $this->assertIsArray($this->_oModule->getReportData('query', $this->_aDay + ['q_dim' => 'browser']));
  112. }
  113. public function testChartKeepsSpellingsOfOneValue()
  114. {
  115. // the tables compare text without case: one value of the table, in every bucket
  116. foreach ([['81.2.69.160', 'Phpunit Case', 0], ['81.2.69.161', 'phpunit case', 3 * 3600], ['81.2.69.162', 'PHPUNIT CASE', 6 * 3600]] as list($sIp, $sTitle, $iAfter))
  117. $this->_collect($this->_server($sIp), ['platform' => 'core-web', 'hits' => [['type' => 'pageview', 'url' => 'http://una.test/case', 'title' => $sTitle]]], self::NOW + $iAfter);
  118. $oFilter = $this->_filter($this->_aDay + ['f' => ['path' => '/case']]);
  119. $this->assertGreaterThan(1, count($oFilter->get()['buckets']));
  120. $a = $this->_oModule->getReports()->getQuery($oFilter, ['q_dim' => 'title', 'q_metric' => 'pageviews']);
  121. $this->assertCount(1, $a['rows']);
  122. $this->assertSame(3, $a['rows'][0]['values']['pageviews']);
  123. $this->assertSame($a['rows'][0]['filter']['value'], $a['chart']['series'][0]['value']);
  124. $this->assertSame(3, array_sum(array_column($a['chart']['series'][0]['points'], 'v')), 'every spelling counts in its bucket');
  125. $this->assertCount(3, array_filter(array_column($a['chart']['series'][0]['points'], 'v')));
  126. }
  127. public function testHostileParametersFallBack()
  128. {
  129. $this->_seed();
  130. $oReports = $this->_oModule->getReports();
  131. $aDefault = $oReports->getQuery($this->_filter($this->_aDay), []);
  132. foreach ([
  133. ['q_dim' => 'path`;DROP TABLE `bx_analytics_events`;--'],
  134. ['q_metric' => 'visitors DESC,(SELECT 1)'],
  135. ['q_dir' => 'asc;--'],
  136. ['q_dim' => ['path'], 'q_metric' => ['visitors'], 'q_dir' => ['asc']],
  137. ['q_dim' => 'e`.`visitor', 'q_metric' => 'COUNT(*)', 'q_dir' => 'DESC'],
  138. ] as $aHostile)
  139. $this->assertEquals($aDefault, $oReports->getQuery($this->_filter($this->_aDay), $aHostile), json_encode($aHostile));
  140. $this->assertSame('visitors', $oReports->getQueryParams(['q_dim' => 'country', 'q_metric' => 'engaged'])['metric'], 'a metric of another kind of dimension');
  141. $this->assertSame('visitors', $oReports->getQueryParams(['q_dim' => 'path', 'q_metric' => 'bounce_rate'])['metric'], 'a column of the table that is not sortable');
  142. $this->assertSame(['country', 'session', 'duration', 'asc'], array_values(array_intersect_key($oReports->getQueryParams(['q_dim' => 'country', 'q_metric' => 'duration', 'q_dir' => 'asc']), array_flip(['dimension', 'kind', 'metric', 'dir']))));
  143. $this->assertSame([], $this->_oModule->_oDb->getBreakdown($this->_filter($this->_aDay)->get(), 'nope', 'visitors', 'DESC', 10));
  144. $this->assertSame([], $this->_oModule->_oDb->getBreakdownSeries($this->_filter($this->_aDay)->get(), 'nope', ['x'], 'day', 0));
  145. $this->assertGreaterThan(0, (int)BxDolDb::getInstance()->getOne("SELECT COUNT(*) FROM `bx_analytics_events` WHERE `path` = '/a' AND `ts` < :to", ['to' => self::NOW + 86400]), 'the events are still there');
  146. }
  147. public function testExport()
  148. {
  149. $this->_seed();
  150. $oReports = $this->_oModule->getReports();
  151. $a = $oReports->getExport('query', $this->_filter($this->_aDay), ['q_dim' => 'country', 'q_metric' => 'pageviews'], 10000);
  152. $this->assertSame('country', $a['dimension']);
  153. $this->assertSame([_t('_bx_analytics_dim_country'), _t('_bx_analytics_txt_raw_value'), _t('_bx_analytics_metric_visitors')], array_slice($a['header'], 0, 3));
  154. $this->assertSame(['GB', 'US', 'FR', 'DE'], array_column($a['rows'], 1), 'the stored values, in the query\'s order');
  155. $this->assertSame($oReports->getLabel('country', 'GB'), $a['rows'][0][0]);
  156. $this->assertSame([2, 2, 4], array_slice($a['rows'][0], 2, 3), 'visitors, sessions, pageviews as numbers');
  157. $this->assertFalse($a['truncated']);
  158. // every value in the query's order, then the row cap
  159. $a = $oReports->getExport('query', $this->_filter($this->_aDay), ['q_dim' => 'path'], 10000);
  160. $this->assertSame(array_column(array_column($oReports->getQuery($this->_filter($this->_aDay), ['q_dim' => 'path'])['rows'], 'filter'), 'value'), array_column($a['rows'], 1));
  161. $this->assertCount(7, array_unique(array_column($a['rows'], 1)));
  162. $a = $oReports->getExport('query', $this->_filter($this->_aDay), ['q_dim' => 'path'], 3);
  163. $this->assertCount(3, $a['rows']);
  164. $this->assertSame(7, $a['total']);
  165. $this->assertTrue($a['truncated']);
  166. $this->assertSame(array_map('_t', ['_bx_analytics_dim_path', '_bx_analytics_txt_raw_value', '_bx_analytics_metric_visitors', '_bx_analytics_metric_pageviews', '_bx_analytics_metric_engaged', '_bx_analytics_metric_entries', '_bx_analytics_metric_bounce_rate', '_bx_analytics_metric_exits']), $a['header'], 'the table\'s columns');
  167. // a table or top list tab exports the same way
  168. $a = $oReports->getExport('table', $this->_filter($this->_aDay), ['table' => 'sources', 'tab_sources' => 'channels'], 100);
  169. $this->assertSame('channel', $a['dimension']);
  170. $a = $oReports->getExport('top', $this->_filter($this->_aDay), ['group' => 'platforms', 'tab' => 'platforms'], 100);
  171. $this->assertSame('platform', $a['dimension']);
  172. $this->assertEqualsCanonicalizing(['core-web', 'next-web', 'rn-ios'], array_column($a['rows'], 1));
  173. $this->assertFalse($oReports->getExport('table', $this->_filter($this->_aDay), ['table' => 'nope'], 100));
  174. $this->assertFalse($oReports->getExport('sessions', $this->_filter($this->_aDay), [], 100));
  175. // through the module: the file name is made of the report, the dimension and the dates only
  176. $this->bxSetLoggedFlags(BX_DOL_ROLE_MEMBER | BX_DOL_ROLE_ADMIN);
  177. $a = $this->_oModule->getReportExport('query', $this->_aDay + ['q_dim' => 'path";x', 'tz' => 'Europe/London']);
  178. $this->assertSame('analytics-query-path-2001-09-09-2001-09-09.csv', $a['filename']);
  179. $a = $this->_oModule->getReportExport('query', ['range' => 'custom', 'from' => '2001-09-09"><x', 'to' => '2001-09-09', 'q_dim' => 'country']);
  180. $this->assertMatchesRegularExpression('/^analytics-query-country-\d{4}-\d{2}-\d{2}-\d{4}-\d{2}-\d{2}\.csv$/', $a['filename'], 'an invalid date falls back to the default period');
  181. $this->assertFalse($this->_oModule->getReportExport('sessions', $this->_aDay), 'no download of a report that names members');
  182. $this->assertFalse($this->_oModule->getReportExport('revenue', $this->_aDay));
  183. // the cells a spreadsheet would run as formulas
  184. $this->_collect($this->_server('81.2.69.150'), ['platform' => 'core-web', 'hits' => [['type' => 'pageview', 'url' => 'http://una.test/x', 'title' => '=HYPERLINK("http://evil.example","open")']]], self::NOW + 300);
  185. $a = $oReports->getExport('query', $this->_filter($this->_aDay), ['q_dim' => 'title'], 100);
  186. $aCells = array_map([BxAnalyticsCsv::class, 'cell'], $a['rows'][array_search('=HYPERLINK("http://evil.example","open")', array_column($a['rows'], 1))]);
  187. $this->assertSame('\'=HYPERLINK("http://evil.example","open")', $aCells[0]);
  188. $this->assertSame('\'=HYPERLINK("http://evil.example","open")', $aCells[1]);
  189. }
  190. public function testAccess()
  191. {
  192. $this->_seed();
  193. // a guest
  194. $this->bxBecomeGuest();
  195. $this->assertFalse($this->_oModule->serviceIsAvaliable());
  196. $this->assertFalse($this->_oModule->getReportExport('query', $this->_aDay));
  197. $this->assertSame(403, $this->_oModule->serviceGetReport('query')['code']);
  198. $this->assertSame('', $this->_oModule->serviceGetBlockReport('query'));
  199. // a member whose level may not use analytics
  200. $iProfile = (int)BxDolDb::getInstance()->getOne("SELECT `p`.`id` FROM `sys_profiles` AS `p` INNER JOIN `sys_accounts` AS `a` ON `a`.`id` = `p`.`account_id` WHERE (`a`.`role` & :admin) = 0 AND `p`.`type` <> 'system' AND `p`.`status` = 'active' ORDER BY `p`.`id` LIMIT 1", ['admin' => BX_DOL_ROLE_ADMIN]);
  201. if (!$iProfile)
  202. $this->markTestSkipped('no member profile to sign in as');
  203. $GLOBALS['glForceCurrentProfileId'] = $iProfile;
  204. $this->bxSetLoggedFlags(BX_DOL_ROLE_MEMBER);
  205. $this->assertFalse(isAdmin());
  206. $iLevel = (int)BxDolAcl::getInstance()->getMemberMembershipInfo($iProfile)['id'];
  207. $this->_grant($iLevel, 'use analytics', false);
  208. $this->assertFalse($this->_oModule->serviceIsAvaliable());
  209. $this->assertFalse($this->_oModule->getReportExport('query', $this->_aDay));
  210. $this->assertSame(403, $this->_oModule->serviceGetReport('query')['code']);
  211. // may use analytics, but not see people
  212. $this->_grant($iLevel, 'use analytics', true);
  213. $this->_grant($iLevel, 'view analytics people', false);
  214. $this->assertTrue($this->_oModule->serviceIsAvaliable());
  215. $this->assertIsArray($this->_oModule->getReportExport('query', $this->_aDay));
  216. $this->assertIsArray($this->_oModule->getReportData('query', $this->_aDay));
  217. $this->assertFalse($this->_oModule->getReportData('sessions', $this->_aDay));
  218. $this->assertFalse($this->_oModule->getReportData('users', $this->_aDay));
  219. $this->assertFalse($this->_oModule->getReportExport('sessions', $this->_aDay));
  220. // a download doesn't count as a use of the countable action
  221. $iBefore = $this->_countActions($iProfile);
  222. $this->_oModule->getReportExport('query', $this->_aDay);
  223. $this->assertSame($iBefore, $this->_countActions($iProfile));
  224. }
  225. public function testServerEvents()
  226. {
  227. $this->assertFalse($this->_oModule->serviceIsSafeService('track'), 'not over the API');
  228. $this->assertFalse($this->_oModule->serviceIsPublicService('track'));
  229. // a custom event joins the member's visit, even with site actions not tracked
  230. $this->_setOption('bx_analytics_track_actions', '');
  231. $iProfileId = 987654321;
  232. $aServer = $this->_server('93.184.216.34', ['REQUEST_METHOD' => 'POST']);
  233. $this->_collect($aServer, ['platform' => 'core-web', 'hits' => [['type' => 'pageview', 'url' => 'http://una.test/import']]], self::NOW, ['profile_id' => $iProfileId]);
  234. $oCollector = new BxAnalyticsCollector($this->_oModule, new BxAnalyticsRequest($aServer), self::NOW + 20);
  235. $this->assertTrue($oCollector->collectAction(self::EVENT, '', 0, $iProfileId, ['type' => 'event', 'platform' => 'core-web', 'props' => ['rows' => 120, 'bad key!' => 1], 'value' => 2.5]));
  236. $this->assertFalse($oCollector->collectAction('login', 'account', 1, $iProfileId, ['platform' => 'core-web']), 'site actions are off');
  237. $aEvent = BxDolDb::getInstance()->getRow("SELECT `e`.*, `s`.`events` AS `session_events` FROM `bx_analytics_events` AS `e` INNER JOIN `bx_analytics_sessions` AS `s` ON `s`.`id` = `e`.`session_id` WHERE `e`.`name` = :name", ['name' => self::EVENT]);
  238. $this->assertSame(['event', 'core-web', '', '{"rows":120}', 1], [$aEvent['type'], $aEvent['platform'], $aEvent['module'], $aEvent['props'], (int)$aEvent['session_events']]);
  239. $this->assertEquals(2.5, $aEvent['value']);
  240. // the service: from code running in any request, here the command line (platform core)
  241. $this->assertFalse($this->_oModule->serviceTrack('bad<name>'));
  242. $this->assertFalse($this->_oModule->serviceTrack(str_repeat('a', 65)));
  243. $this->assertFalse($this->_oModule->serviceTrack(['x']));
  244. $this->assertTrue($this->_oModule->serviceTrack(self::EVENT, ['plan' => 'pro']));
  245. $this->assertSame('core', BxDolDb::getInstance()->getOne("SELECT `platform` FROM `bx_analytics_events` WHERE `name` = :name AND `type` = 'event' AND `session_id` = 0", ['name' => self::EVENT]));
  246. // a platform left out of Settings > Platforms stores nothing, as Setup shows it
  247. $iEvents = (int)BxDolDb::getInstance()->getOne("SELECT COUNT(*) FROM `bx_analytics_events` WHERE `name` = :name", ['name' => self::EVENT]);
  248. $this->_setOption('bx_analytics_platforms', 'core-web,next-web,rn-ios,rn-android,api');
  249. $this->assertFalse($this->_oModule->serviceTrack(self::EVENT), 'the server platform is off');
  250. $this->assertFalse($oCollector->collectAction(self::EVENT, '', 0, $iProfileId, ['type' => 'event', 'platform' => 'core']));
  251. $this->_setOption('bx_analytics_platforms', 'next-web');
  252. $this->assertFalse($oCollector->collectAction(self::EVENT, '', 0, $iProfileId, ['type' => 'event', 'platform' => 'core-web']), 'the classic site is off');
  253. $this->assertSame($iEvents, (int)BxDolDb::getInstance()->getOne("SELECT COUNT(*) FROM `bx_analytics_events` WHERE `name` = :name", ['name' => self::EVENT]));
  254. $this->assertSame('off', $this->_oModule->getSetupStatus(self::NOW, false)['core']['status']);
  255. $this->_setOption('bx_analytics_platforms', 'core-web,next-web,rn-ios,rn-android,api,core');
  256. $this->_setOption('bx_analytics_tracking', '');
  257. $this->assertFalse($this->_oModule->serviceTrack(self::EVENT), 'tracking is off');
  258. }
  259. /**
  260. * Seven visitors on 2001-09-09: GB (4 pageviews), US (3, one on iOS), FR (2) and DE (1,
  261. * on NEO web); seven pages, /a seen by three, /c by two.
  262. */
  263. protected function _seed()
  264. {
  265. $aVisits = [
  266. ['81.2.69.142', 'GB', 'core-web', ['/a', '/b', '/d'], 'https://www.google.com/'],
  267. ['81.2.69.143', 'GB', 'core-web', ['/a'], ''],
  268. ['93.184.216.34', 'US', 'rn-ios', ['/a', '/c'], ''],
  269. ['93.184.216.35', 'US', 'core-web', ['/c'], ''],
  270. ['93.184.216.36', 'DE', 'next-web', ['/e'], ''],
  271. ['93.184.216.37', 'FR', 'core-web', ['/f'], ''],
  272. ['93.184.216.38', 'FR', 'core-web', ['/g'], ''],
  273. ];
  274. foreach ($aVisits as $i => list($sIp, $sCountry, $sPlatform, $aPages, $sReferrer)) {
  275. $aServer = $this->_server($sIp, ['HTTP_CF_IPCOUNTRY' => $sCountry], $sPlatform == 'rn-ios' ? 'NEO/1 CFNetwork/1.0 Darwin/24.0.0' : self::UA_SAFARI);
  276. $sHost = $sPlatform == 'rn-ios' ? '' : 'http://una.test';
  277. $iTime = self::NOW + $i * 600;
  278. foreach ($aPages as $j => $sPage) {
  279. $aHits = [];
  280. // the previous page reports its engaged time when the next one opens
  281. if ($j > 0)
  282. $aHits[] = ['type' => 'leave', 'url' => $sHost . $aPages[$j - 1], 'engaged' => 5000 * ($i + 1)];
  283. $aHits[] = ['type' => 'pageview', 'url' => $sHost . $sPage, 'title' => 'Page ' . $sPage];
  284. $this->_collect($aServer, ['platform' => $sPlatform, 'referrer' => $j ? '' : $sReferrer] + ($sPlatform == 'rn-ios' ? ['app' => 'com.neo.so'] : []) + ['hits' => $aHits], $iTime + 30 * $j);
  285. }
  286. }
  287. }
  288. protected function _countActions($iProfileId)
  289. {
  290. return (int)BxDolDb::getInstance()->getOne("SELECT IFNULL(SUM(`ActionsLeft`), 0) + COUNT(*) FROM `sys_acl_actions_track` WHERE `IDMember` = :id", ['id' => (int)$iProfileId]);
  291. }
  292. protected function _collect($aServer, $aPayload, $iNow, $aContext = [])
  293. {
  294. $oCollector = new BxAnalyticsCollector($this->_oModule, new BxAnalyticsRequest($aServer), $iNow);
  295. return $oCollector->collect($aPayload, $aContext + ['platform' => 'api']);
  296. }
  297. protected function _filter($aParams)
  298. {
  299. return new BxAnalyticsFilter($this->_oModule, $aParams, self::NOW + 86400);
  300. }
  301. protected function _server($sIp, $aExtra = [], $sUserAgent = self::UA_SAFARI)
  302. {
  303. return array_merge(['REMOTE_ADDR' => $sIp, 'HTTP_USER_AGENT' => $sUserAgent], $aExtra);
  304. }
  305. protected function _setOption($sName, $sValue)
  306. {
  307. if (!array_key_exists($sName, $this->_aOptions))
  308. $this->_aOptions[$sName] = getParam($sName);
  309. setParam($sName, $sValue);
  310. }
  311. protected function _grant($iLevel, $sAction, $bAllowed)
  312. {
  313. $oDb = BxDolDb::getInstance();
  314. $iAction = (int)$oDb->getOne("SELECT `ID` FROM `sys_acl_actions` WHERE `Module` = 'bx_analytics' AND `Name` = :name", ['name' => $sAction]);
  315. $aBindings = ['level' => (int)$iLevel, 'action' => $iAction];
  316. $sKey = $iLevel . ':' . $iAction;
  317. if (!array_key_exists($sKey, $this->_aMatrix))
  318. $this->_aMatrix[$sKey] = $oDb->getRow("SELECT * FROM `sys_acl_matrix` WHERE `IDLevel` = :level AND `IDAction` = :action", $aBindings);
  319. $oDb->query("DELETE FROM `sys_acl_matrix` WHERE `IDLevel` = :level AND `IDAction` = :action", $aBindings);
  320. if ($bAllowed)
  321. $oDb->query("INSERT INTO `sys_acl_matrix` SET `IDLevel` = :level, `IDAction` = :action", $aBindings);
  322. $oDb->cleanMemoryAll();
  323. }
  324. protected function _restoreMatrix()
  325. {
  326. $oDb = BxDolDb::getInstance();
  327. foreach ($this->_aMatrix as $sKey => $aRow) {
  328. list($iLevel, $iAction) = explode(':', $sKey);
  329. $oDb->query("DELETE FROM `sys_acl_matrix` WHERE `IDLevel` = :level AND `IDAction` = :action", ['level' => (int)$iLevel, 'action' => (int)$iAction]);
  330. if (!empty($aRow))
  331. $oDb->query("INSERT INTO `sys_acl_matrix` SET " . $oDb->arrayToSQL(array_filter($aRow, function ($m) { return $m !== null; })));
  332. }
  333. $this->_aMatrix = [];
  334. $oDb->cleanMemoryAll();
  335. }
  336. protected function _cleanup()
  337. {
  338. $oDb = BxDolDb::getInstance();
  339. $iTo = self::NOW + 30 * 86400;
  340. $oDb->query("DELETE FROM `bx_analytics_events` WHERE `ts` < :to OR (`type` = 'event' AND `name` = :name)", ['to' => $iTo, 'name' => self::EVENT]);
  341. $oDb->query("DELETE FROM `bx_analytics_sessions` WHERE `started` < :to", ['to' => $iTo]);
  342. $oDb->query("DELETE FROM `bx_analytics_salts` WHERE `day` < '2002-01-01'");
  343. $oDb->query("DELETE FROM `bx_analytics_limits` WHERE `bucket` < 1009843200");
  344. $this->_oModule->cleanReportCache(); // reports of the test data cached by the run before
  345. }
  346. }