BxAnalyticsTemplate.php 86 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup Analytics Analytics
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. bx_import ('BxDolModuleTemplate');
  12. class BxAnalyticsTemplate extends BxBaseModGeneralTemplate
  13. {
  14. /**
  15. * Blocks are drawn inside Studio: assets go to the Studio template.
  16. */
  17. protected $_bStudio = false;
  18. protected $_aRanges = ['today', 'yesterday', '24h', '7d', '30d', '90d', '12m'];
  19. function __construct(&$oConfig, &$oDb)
  20. {
  21. $this->MODULE = 'bx_analytics';
  22. parent::__construct($oConfig, $oDb);
  23. }
  24. public function setStudio($bStudio)
  25. {
  26. $this->_bStudio = (bool)$bStudio;
  27. }
  28. public function isStudio()
  29. {
  30. return $this->_bStudio;
  31. }
  32. function getCanvas()
  33. {
  34. $aTmp = $this->getModule()->getSelectedModules();
  35. $aModules = $aTmp[0];
  36. $aModulesList = $aTmp[1];
  37. $aModulesList2 = [];
  38. foreach($aModules as $sModule)
  39. $aModulesList2[] = ['value' => $sModule, 'title' => $aModulesList[$sModule]];
  40. $iDaysBefore = getParam('bx_analytics_default_interval_day');
  41. $sDate = date('d/m/Y', time() - $iDaysBefore * 86400) . ' - ' . date('d/m/Y');
  42. $aJs = ['chart.umd.min.js', 'chartjs-adapter-moment.min.js', 'analytics.js', BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/plugins/daterangepicker/|daterangepicker.js', BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/plugins/datatables/|datatables.min.js'];
  43. $aCss = ['main.css', BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/plugins/daterangepicker/|daterangepicker.css', BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/plugins/datatables/|datatables.min.css'];
  44. $this->_addAssets($aJs, $aCss);
  45. return $this->getJsCode('analytics') . $this->parseHtmlByName('canvas.html', [
  46. 'bx_repeat:items' => $aModulesList2,
  47. 'interval' => $sDate,
  48. 'export_to_csv_title' => _t('_bx_analytics_txt_export_to_csv_title')
  49. ]);
  50. }
  51. /**
  52. * The classic UI tracker: a small deferred script configured by data attributes.
  53. */
  54. public function getTrackerCode()
  55. {
  56. return $this->parseHtmlByName('tracker.html', [
  57. 'src' => $this->_getTrackerUrl(),
  58. 'endpoint' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'collect',
  59. 'platform' => BX_ANALYTICS_PLATFORM_CORE_WEB,
  60. 'identity' => $this->_oConfig->getIdentityMode(),
  61. 'dnt' => $this->_oConfig->isRespectDnt() ? 1 : 0,
  62. 'errors' => $this->_oConfig->isTrackErrors() ? 1 : 0,
  63. // percent of page loads that measure vitals, 0 for none
  64. 'vitals' => $this->_oConfig->isTrackVitals() ? $this->_oConfig->getVitalsSample() : 0,
  65. ]);
  66. }
  67. /**
  68. * The tracker's URL. Every classic page loads it, so when the site minifies its scripts
  69. * it is minified too, once per revision, into the public cache next to the site's
  70. * scripts (clearing the JS cache removes it); the source file otherwise, or when the
  71. * cache can't be written.
  72. */
  73. protected function _getTrackerUrl()
  74. {
  75. $iRevision = (int)getParam('sys_revision');
  76. $sSource = 'boonex/analytics/js/tracker.js';
  77. $sUrl = BX_DOL_URL_MODULES . $sSource . '?v=' . $iRevision;
  78. if(bx_is_dbg() || getParam('sys_template_cache_js_enable') != 'on' || getParam('sys_template_cache_minify_js_enable') != 'on')
  79. return $sUrl;
  80. $sName = $this->getCacheFilePrefix('js') . 'bx_analytics_tracker_' . bx_site_hash($sSource . $iRevision) . '.js';
  81. $sPath = BX_DIRECTORY_PATH_CACHE_PUBLIC . $sName;
  82. if(!file_exists($sPath)) {
  83. $sCode = @file_get_contents(BX_DIRECTORY_PATH_MODULES . $sSource);
  84. $sMinified = $sCode !== false ? BxDolMinify::getInstance()->minifyJs($sCode) : '';
  85. // written aside and moved in, so a page never loads a half-written file
  86. $sTemp = $sPath . '.' . uniqid() . '.tmp';
  87. if($sMinified === '' || @file_put_contents($sTemp, $sMinified) === false || !@rename($sTemp, $sPath)) {
  88. @unlink($sTemp);
  89. return $sUrl;
  90. }
  91. @chmod($sPath, BX_DOL_FILE_RIGHTS);
  92. }
  93. return BX_DOL_URL_CACHE_PUBLIC . $sName;
  94. }
  95. /**
  96. * A report block on a page: the dashboard script, once per page, and the report.
  97. *
  98. * @param string $sReport report name, see BxAnalyticsModule::getReportData
  99. * @param array $aData report data
  100. * @param object $oFilter the filter the data was built with
  101. * @param array $aParams block parameters (group, table...) the script sends back on reload
  102. */
  103. public function getBlock($sReport, $aData, $oFilter, $aParams = [])
  104. {
  105. $this->_addDashboardAssets($sReport);
  106. return $this->_getJsInit() . $this->getReportHtml($sReport, $aData, $oFilter, $aParams);
  107. }
  108. /**
  109. * Block HTML for a report, on first render and when the dashboard script reloads it.
  110. */
  111. public function getReportHtml($sReport, $aData, $oFilter, $aParams = [])
  112. {
  113. $sMethod = '_get' . bx_gen_method_name($sReport) . 'Html';
  114. if(!method_exists($this, $sMethod))
  115. return '';
  116. $sContent = $this->$sMethod($aData, $oFilter, $aParams);
  117. if($sContent === '')
  118. return '';
  119. return $this->parseHtmlByName('block.html', [
  120. 'report' => $sReport,
  121. 'params' => $this->_esc(json_encode((object)$aParams)),
  122. 'class' => str_replace('_', '-', $sReport),
  123. 'content' => $sContent,
  124. ]);
  125. }
  126. /**
  127. * A Studio section: blocks in a grid, the wide ones across all columns.
  128. *
  129. * @param array $aBlocks list of ['title' => heading or '', 'content' => HTML, 'wide' => bool]
  130. */
  131. public function getStudioSection($aBlocks)
  132. {
  133. $aCards = [];
  134. foreach($aBlocks as $aBlock) {
  135. if($aBlock['content'] === '' || $aBlock['content'] === [])
  136. continue;
  137. $aCards[] = [
  138. 'class' => !empty($aBlock['wide']) ? 'bx-analytics-studio-wide' : '',
  139. 'bx_if:title' => [
  140. 'condition' => !empty($aBlock['title']),
  141. 'content' => ['title' => !empty($aBlock['title']) ? $aBlock['title'] : ''],
  142. ],
  143. 'content' => $aBlock['content'],
  144. ];
  145. }
  146. return $this->parseHtmlByName('studio_section.html', [
  147. 'bx_repeat:cards' => $aCards,
  148. ]);
  149. }
  150. /**
  151. * Traffic block of the Studio dashboard.
  152. */
  153. public function getStudioDashboardTraffic($aOverview, $aRealtime, $aPlatforms)
  154. {
  155. $this->addStudioCss(['dashboard.css']);
  156. $aValues = array_map(function($a) { return $a['future'] ? null : $a['visitors']; }, $aOverview['series']);
  157. $iMax = max(array_merge([1], array_map('intval', $aValues)));
  158. $iCount = count($aValues);
  159. $aPoints = [];
  160. foreach($aValues as $i => $mixedValue)
  161. if($mixedValue !== null)
  162. $aPoints[] = round($iCount > 1 ? 100 * $i / ($iCount - 1) : 50, 2) . ',' . round(38 - 36 * $mixedValue / $iMax, 2);
  163. $aKpis = [];
  164. foreach(['visitors', 'pageviews'] as $sMetric) {
  165. $aKpi = $aOverview['kpis'][$sMetric];
  166. $aKpis[] = [
  167. 'title' => _t('_bx_analytics_metric_' . $sMetric),
  168. 'value' => $this->formatValue($sMetric, $aKpi['value']),
  169. 'change' => $this->_formatChange($aKpi['change']),
  170. 'trend' => $this->_trend($sMetric, $aKpi['change']),
  171. ];
  172. }
  173. $aRows = [];
  174. foreach($aPlatforms['items'] as $aItem)
  175. $aRows[] = [
  176. 'label' => $this->_esc($aItem['label']),
  177. 'share' => $aItem['share'],
  178. 'bar' => $aItem['bar'],
  179. ];
  180. return $this->parseHtmlByName('studio_traffic.html', [
  181. 'online' => $this->formatValue('number', $aRealtime['online']),
  182. 'online_title' => _t('_bx_analytics_txt_online_now', $aRealtime['window']),
  183. 'bx_repeat:kpis' => $aKpis,
  184. 'points' => implode(' ', $aPoints),
  185. 'chart_label' => _t('_bx_analytics_txt_chart_visitors_7d'),
  186. 'bx_if:platforms' => [
  187. 'condition' => !empty($aRows),
  188. 'content' => ['bx_repeat:platforms' => $aRows],
  189. ],
  190. 'bx_if:empty' => [
  191. 'condition' => empty($aRows),
  192. 'content' => ['empty' => _t('_bx_analytics_txt_no_data_yet')],
  193. ],
  194. 'link' => BX_DOL_URL_STUDIO . 'module.php?name=' . $this->MODULE . '&page=overview',
  195. 'link_title' => _t('_bx_analytics_txt_open_analytics'),
  196. ]);
  197. }
  198. /**
  199. * A number for display by its kind: number (950, 9.5K), percent, duration (seconds),
  200. * ms, pages_per_session, or a metric name.
  201. */
  202. public function formatValue($sType, $mixedValue)
  203. {
  204. if($mixedValue === null || $mixedValue === '')
  205. return '';
  206. switch($sType) {
  207. case 'bounce_rate':
  208. case 'percent':
  209. case 'rate':
  210. return (abs($mixedValue) < 10 && round($mixedValue) != $mixedValue ? number_format((float)$mixedValue, 1) : round($mixedValue)) . '%';
  211. // shares close to 100%, such as crash-free visits: 99.95%, not 100%
  212. case 'percent_precise':
  213. return rtrim(rtrim(number_format((float)$mixedValue, 2), '0'), '.') . '%';
  214. case 'pages_per_session':
  215. case 'decimal':
  216. return number_format((float)$mixedValue, 1);
  217. // good, needs_improvement or poor, as a coloured label
  218. case 'rating':
  219. return $this->parseHtmlByName('rating.html', [
  220. 'rating' => str_replace('_', '-', $this->_esc($mixedValue)),
  221. 'title' => _t('_bx_analytics_rating_' . preg_replace('/[^a-z_]/', '', (string)$mixedValue)),
  222. ]);
  223. // a small line of values over time
  224. case 'spark':
  225. return is_array($mixedValue) ? $this->_getSparkHtml($mixedValue) : '';
  226. case 'duration':
  227. case 'engaged':
  228. $iSeconds = (int)round($mixedValue);
  229. if($iSeconds < 60)
  230. return $iSeconds . _t('_bx_analytics_txt_duration_s');
  231. if($iSeconds < 3600)
  232. return intdiv($iSeconds, 60) . _t('_bx_analytics_txt_duration_m') . ' ' . ($iSeconds % 60) . _t('_bx_analytics_txt_duration_s');
  233. return intdiv($iSeconds, 3600) . _t('_bx_analytics_txt_duration_h') . ' ' . intdiv($iSeconds % 3600, 60) . _t('_bx_analytics_txt_duration_m');
  234. case 'ms':
  235. return $mixedValue >= 1000 ? number_format($mixedValue / 1000, 2) . _t('_bx_analytics_txt_duration_s') : round($mixedValue) . _t('_bx_analytics_txt_ms');
  236. case 'cls':
  237. return number_format((float)$mixedValue, 3);
  238. case 'money':
  239. // the site's default currency, the way the payment module shows it
  240. return $this->_esc(_t_format_currency((float)$mixedValue));
  241. case 'credits':
  242. return rtrim(rtrim(number_format((float)$mixedValue, 2), '0'), '.');
  243. case 'datetime':
  244. return bx_time_js((int)$mixedValue, BX_FORMAT_DATE_TIME, true);
  245. case 'date':
  246. return bx_time_js((int)$mixedValue, BX_FORMAT_DATE, true);
  247. case 'text':
  248. return $this->_esc($mixedValue);
  249. }
  250. return $this->_formatNumber($mixedValue);
  251. }
  252. protected function _getFiltersHtml($aData, $oFilter)
  253. {
  254. $aFilter = $oFilter->get();
  255. $aRanges = [];
  256. foreach($this->_aRanges as $sRange)
  257. $aRanges[] = [
  258. 'value' => $sRange,
  259. 'title' => _t('_bx_analytics_range_' . $sRange),
  260. 'selected' => $aFilter['range'] == $sRange ? 'selected="selected"' : '',
  261. ];
  262. if($aFilter['range'] == 'custom')
  263. $aRanges[] = [
  264. 'value' => 'custom',
  265. 'title' => $this->_esc($aFilter['from_date'] . ' - ' . $aFilter['to_date']),
  266. 'selected' => 'selected="selected"',
  267. ];
  268. $aPlatforms = [['value' => '', 'title' => _t('_bx_analytics_txt_all_platforms'), 'selected' => '']];
  269. foreach($this->_oConfig->getPlatforms() as $sCode => $sTitle)
  270. $aPlatforms[] = [
  271. 'value' => $sCode,
  272. 'title' => _t($sTitle),
  273. 'selected' => $aFilter['platform'] == $sCode ? 'selected="selected"' : '',
  274. ];
  275. $aChips = [];
  276. foreach($aData['chips'] as $aChip)
  277. $aChips[] = [
  278. 'name' => $aChip['name'],
  279. 'title' => $aChip['title'],
  280. 'value' => $this->_esc($aChip['label']),
  281. 'remove' => $this->_esc(_t('_bx_analytics_txt_remove_filter', $aChip['title'])),
  282. ];
  283. return $this->parseHtmlByName('filters.html', [
  284. 'range_label' => _t('_bx_analytics_txt_range'),
  285. 'bx_repeat:ranges' => $aRanges,
  286. 'platform_label' => _t('_bx_analytics_txt_platform'),
  287. 'bx_repeat:platforms' => $aPlatforms,
  288. 'bx_if:chips' => [
  289. 'condition' => !empty($aChips),
  290. 'content' => ['bx_repeat:chips' => $aChips],
  291. ],
  292. 'online' => $this->formatValue('number', $aData['online']),
  293. 'online_title' => _t('_bx_analytics_txt_online_now', $aData['window']),
  294. ]);
  295. }
  296. protected function _getOverviewHtml($aOverview, $oFilter)
  297. {
  298. $sMetric = bx_get('metric');
  299. $aMetrics = $this->getModule()->getReports()->getMetrics();
  300. if(!in_array($sMetric, $aMetrics, true))
  301. $sMetric = 'visitors';
  302. $aKpis = [];
  303. foreach($aOverview['kpis'] as $sName => $aKpi)
  304. $aKpis[] = [
  305. 'metric' => $sName,
  306. 'pressed' => $sName == $sMetric ? 'true' : 'false',
  307. 'title' => _t('_bx_analytics_metric_' . $sName),
  308. 'value' => $this->formatValue($sName, $aKpi['value']),
  309. 'change' => $this->_formatChange($aKpi['change']),
  310. 'change_title' => $this->_esc(_t('_bx_analytics_txt_previous_period', $this->formatValue($sName, $aKpi['previous']))),
  311. 'trend' => $this->_trend($sName, $aKpi['change']),
  312. ];
  313. $aTitles = [];
  314. foreach($aMetrics as $sName)
  315. $aTitles[$sName] = _t('_bx_analytics_metric_' . $sName);
  316. return $this->parseHtmlByName('overview.html', [
  317. 'kpis_label' => _t('_bx_analytics_txt_key_figures'),
  318. 'bx_repeat:kpis' => $aKpis,
  319. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_label')),
  320. 'chart_json' => $this->_json([
  321. 'metric' => $sMetric,
  322. 'bucket' => $aOverview['bucket'],
  323. 'tz' => $aOverview['tz'],
  324. 'series' => $aOverview['series'],
  325. 'titles' => $aTitles,
  326. ]),
  327. ]);
  328. }
  329. protected function _getTopHtml($aTop, $oFilter)
  330. {
  331. $aItems = [];
  332. foreach($aTop['items'] as $aItem)
  333. $aItems[] = [
  334. 'dimension' => $aTop['dimension'],
  335. 'value' => $this->_esc($aItem['value']),
  336. 'label' => $this->_esc($aItem['label']),
  337. 'visitors' => $this->formatValue('number', $aItem['visitors']),
  338. 'share' => $aItem['share'],
  339. 'bar' => $aItem['bar'],
  340. 'filter_title' => $this->_esc(_t('_bx_analytics_txt_filter_by', $aItem['label'])),
  341. ];
  342. return $this->parseHtmlByName('top.html', [
  343. 'tabs' => $this->_getTabsHtml($aTop['group'], $aTop['tabs']),
  344. 'group' => $aTop['group'],
  345. 'column' => $aTop['column'],
  346. 'visitors' => _t('_bx_analytics_metric_visitors'),
  347. 'bx_if:items' => [
  348. 'condition' => !empty($aItems),
  349. 'content' => ['bx_repeat:items' => $aItems],
  350. ],
  351. 'bx_if:empty' => [
  352. 'condition' => empty($aItems),
  353. 'content' => ['empty' => _t('_bx_analytics_txt_no_data')],
  354. ],
  355. ]);
  356. }
  357. protected function _getRealtimeHtml($aData, $oFilter)
  358. {
  359. $iMax = 1;
  360. foreach($aData['minutes'] as $aMinute)
  361. $iMax = max($iMax, $aMinute['pageviews']);
  362. $aMinutes = [];
  363. foreach($aData['minutes'] as $aMinute)
  364. $aMinutes[] = [
  365. 'height' => $aMinute['pageviews'] ? max(4, round(100 * $aMinute['pageviews'] / $iMax)) : 0,
  366. 'title' => $this->_esc(_t('_bx_analytics_txt_pageviews_at', $aMinute['pageviews'], gmdate('H:i', $aMinute['t']) . ' UTC')),
  367. ];
  368. $aPlatforms = [];
  369. foreach($aData['platforms'] as $aPlatform)
  370. $aPlatforms[] = [
  371. 'label' => $this->_esc($aPlatform['label']),
  372. 'visitors' => $this->formatValue('number', $aPlatform['visitors']),
  373. 'share' => $aPlatform['share'],
  374. ];
  375. $aRecent = [];
  376. foreach($aData['recent'] as $aItem) {
  377. $aMeta = array_filter([
  378. $aItem['platform_label'],
  379. $aItem['city'] !== '' ? $aItem['city'] . ', ' . $aItem['country_label'] : ($aItem['country'] !== '' ? $aItem['country_label'] : ''),
  380. $aItem['device'],
  381. $aItem['member'] ? _t('_bx_analytics_txt_member') : '',
  382. ]);
  383. $aRecent[] = [
  384. 'time' => bx_time_js($aItem['ts'], BX_FORMAT_DATE_TIME),
  385. 'title' => $this->_esc($aItem['title'] !== '' ? $aItem['title'] : $aItem['path']),
  386. 'path' => $this->_esc($aItem['path']),
  387. 'meta' => $this->_esc(implode(' · ', $aMeta)),
  388. ];
  389. }
  390. return $this->parseHtmlByName('realtime.html', [
  391. 'online' => $this->formatValue('number', $aData['online']),
  392. 'online_title' => _t('_bx_analytics_txt_online_now', $aData['window']),
  393. 'bx_if:platforms' => [
  394. 'condition' => !empty($aPlatforms),
  395. 'content' => ['bx_repeat:platforms' => $aPlatforms],
  396. ],
  397. 'minutes_label' => $this->_esc(_t('_bx_analytics_txt_pageviews_30m')),
  398. 'bx_repeat:minutes' => $aMinutes,
  399. 'axis_start' => _t('_bx_analytics_txt_minutes_ago', 30),
  400. 'axis_end' => _t('_bx_analytics_txt_now'),
  401. 'recent_title' => _t('_bx_analytics_txt_latest_pageviews'),
  402. 'bx_if:recent' => [
  403. 'condition' => !empty($aRecent),
  404. 'content' => ['bx_repeat:recent' => $aRecent],
  405. ],
  406. 'bx_if:empty' => [
  407. 'condition' => empty($aRecent),
  408. 'content' => ['empty' => _t('_bx_analytics_txt_no_recent')],
  409. ],
  410. ]);
  411. }
  412. protected function _getMapHtml($aData, $oFilter)
  413. {
  414. $aCountries = [];
  415. foreach(array_slice($aData['countries'], 0, 12) as $aCountry)
  416. $aCountries[] = [
  417. 'value' => $this->_esc($aCountry['country']),
  418. 'label' => $this->_esc($aCountry['label']),
  419. 'visitors' => $this->formatValue('number', $aCountry['visitors']),
  420. 'share' => $aCountry['share'],
  421. 'bar' => $aCountry['bar'],
  422. 'filter_title' => $this->_esc(_t('_bx_analytics_txt_filter_by', $aCountry['label'])),
  423. ];
  424. return $this->parseHtmlByName('map.html', [
  425. 'tabs' => $this->_getTabsHtml('map', $aData['tabs']),
  426. 'refresh' => $aData['tab'] == 'live' ? 'data-bx-analytics-refresh="30"' : '',
  427. 'map_label' => $this->_esc(_t('_bx_analytics_txt_map_label')),
  428. 'map_json' => $this->_json([
  429. 'countries' => $aData['countries'],
  430. 'points' => $aData['points'],
  431. 'provider' => $aData['provider'],
  432. 'visitors' => _t('_bx_analytics_metric_visitors'),
  433. ]),
  434. 'column' => _t('_bx_analytics_tab_countries'),
  435. 'visitors' => _t('_bx_analytics_metric_visitors'),
  436. 'bx_if:countries' => [
  437. 'condition' => !empty($aCountries),
  438. 'content' => ['bx_repeat:countries' => $aCountries],
  439. ],
  440. 'bx_if:empty' => [
  441. 'condition' => empty($aCountries),
  442. 'content' => ['empty' => _t($aData['tab'] == 'live' ? '_bx_analytics_txt_no_recent' : '_bx_analytics_txt_no_data')],
  443. ],
  444. 'bx_if:unknown' => [
  445. 'condition' => $aData['unknown'] > 0,
  446. 'content' => ['unknown' => _t('_bx_analytics_txt_map_unknown', $this->formatValue('number', $aData['unknown']))],
  447. ],
  448. ]);
  449. }
  450. protected function _getBotsHtml($aData, $oFilter)
  451. {
  452. return $this->parseHtmlByName('bots.html', [
  453. 'tabs' => $this->_getTabsHtml('bots', $aData['tabs']),
  454. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  455. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_bots')),
  456. 'chart_json' => $this->_json($aData['chart']),
  457. 'bots' => $this->_getTableHtml($aData['bots'], $oFilter),
  458. 'pages_title' => $aData['pages']['caption'],
  459. 'pages' => $this->_getTableHtml($aData['pages'], $oFilter),
  460. ]);
  461. }
  462. protected function _getEventsHtml($aData, $oFilter)
  463. {
  464. if($aData['view'] == 'list')
  465. return $this->_getTableHtml($aData, $oFilter);
  466. $aProps = [];
  467. foreach($aData['props'] as $aTable)
  468. $aProps[] = [
  469. 'title' => $this->_esc($aTable['caption']),
  470. 'table' => $this->_getTableHtml($aTable, $oFilter),
  471. ];
  472. return $this->parseHtmlByName('event.html', [
  473. 'back' => _t('_bx_analytics_txt_back_events'),
  474. 'title' => $this->_esc($aData['title']),
  475. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  476. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_event', $aData['title'])),
  477. 'chart_json' => $this->_json($aData['chart']),
  478. 'platforms_title' => _t('_bx_analytics_tab_platforms'),
  479. 'platforms' => $this->_getBarsHtml($aData['platforms'], 'count'),
  480. 'bx_repeat:props' => $aProps,
  481. 'bx_if:no_props' => [
  482. 'condition' => empty($aProps),
  483. 'content' => [
  484. 'props_title' => _t('_bx_analytics_txt_properties'),
  485. 'no_props' => _t('_bx_analytics_txt_no_properties'),
  486. ],
  487. ],
  488. ]);
  489. }
  490. protected function _getSessionsHtml($aData, $oFilter)
  491. {
  492. if($aData['view'] == 'list')
  493. return $this->_getTableHtml($aData, $oFilter);
  494. return $this->_getSessionHtml($aData);
  495. }
  496. protected function _getUsersHtml($aData, $oFilter)
  497. {
  498. if($aData['view'] == 'list')
  499. return $this->_getTableHtml($aData, $oFilter);
  500. if($aData['view'] == 'detail')
  501. return $this->_getSessionHtml($aData);
  502. return $this->parseHtmlByName('user.html', [
  503. 'back' => _t('_bx_analytics_txt_back_members'),
  504. 'member' => $this->_getMemberHtml($aData['member']),
  505. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  506. 'sessions_title' => _t('_bx_analytics_lmi_cpt_sessions'),
  507. 'sessions' => $this->_getTableHtml($aData['sessions'], $oFilter),
  508. 'platforms_title' => _t('_bx_analytics_tab_platforms'),
  509. 'platforms' => $this->_getBarsHtml($aData['platforms'], 'count'),
  510. 'actions_title' => $aData['actions']['caption'],
  511. 'actions' => $this->_getTableHtml($aData['actions'], $oFilter),
  512. ]);
  513. }
  514. protected function _getSessionHtml($aData)
  515. {
  516. $aFacts = [];
  517. foreach($aData['facts'] as $aFact)
  518. $aFacts[] = [
  519. 'title' => $aFact['title'],
  520. 'value' => $aFact['value'] === '' ? '&mdash;' : $this->formatValue($aFact['type'], $aFact['value']),
  521. ];
  522. $aKinds = [
  523. BX_ANALYTICS_HIT_PAGEVIEW => _t('_bx_analytics_event_type_pageview'),
  524. BX_ANALYTICS_HIT_EVENT => _t('_bx_analytics_event_type_event'),
  525. BX_ANALYTICS_HIT_UNA => _t('_bx_analytics_event_type_una'),
  526. ];
  527. $aTimeline = [];
  528. foreach($aData['timeline'] as $aItem)
  529. $aTimeline[] = [
  530. 'type' => $aItem['type'],
  531. 'offset' => sprintf('%d:%02d', intdiv($aItem['offset'], 60), $aItem['offset'] % 60),
  532. 'kind' => isset($aKinds[$aItem['type']]) ? $aKinds[$aItem['type']] : $this->_esc($aItem['type']),
  533. 'label' => $this->_esc($aItem['label']),
  534. 'bx_if:path' => [
  535. 'condition' => $aItem['path'] !== '' && $aItem['path'] !== $aItem['label'],
  536. 'content' => ['path' => $this->_esc($aItem['path'])],
  537. ],
  538. 'engaged' => $aItem['engaged'] ? $this->formatValue('duration', $aItem['engaged']) : '',
  539. ];
  540. return $this->parseHtmlByName('session.html', [
  541. 'back' => _t('_bx_analytics_txt_back_sessions'),
  542. 'member' => $this->_getMemberHtml($aData['member']),
  543. 'bx_repeat:facts' => $aFacts,
  544. 'timeline_title' => _t('_bx_analytics_txt_timeline'),
  545. 'bx_if:timeline' => [
  546. 'condition' => !empty($aTimeline),
  547. 'content' => ['bx_repeat:timeline' => $aTimeline],
  548. ],
  549. 'bx_if:empty' => [
  550. 'condition' => empty($aTimeline),
  551. 'content' => ['empty' => _t('_bx_analytics_txt_no_data')],
  552. ],
  553. ]);
  554. }
  555. protected function _getMemberHtml($aMember)
  556. {
  557. $bMember = !empty($aMember);
  558. return $this->parseHtmlByName('member.html', [
  559. 'bx_if:thumb' => [
  560. 'condition' => $bMember && !empty($aMember['thumb']),
  561. 'content' => ['thumb' => $bMember ? $this->_esc($aMember['thumb']) : ''],
  562. ],
  563. 'bx_if:no_thumb' => [
  564. 'condition' => !$bMember || empty($aMember['thumb']),
  565. 'content' => [],
  566. ],
  567. 'bx_if:link' => [
  568. 'condition' => $bMember,
  569. 'content' => [
  570. 'url' => $bMember ? $this->_esc($aMember['url']) : '',
  571. 'name' => $bMember ? $this->_esc($aMember['name']) : '',
  572. ],
  573. ],
  574. 'bx_if:text' => [
  575. 'condition' => !$bMember,
  576. 'content' => ['name' => _t('_bx_analytics_txt_guest')],
  577. ],
  578. ]);
  579. }
  580. /**
  581. * Shares as bars: items with label, a count under `$sKey` and share in percent.
  582. */
  583. protected function _getBarsHtml($aItems, $sKey)
  584. {
  585. $iMax = 1;
  586. foreach($aItems as $aItem)
  587. $iMax = max($iMax, (int)$aItem[$sKey]);
  588. $aRows = [];
  589. foreach($aItems as $aItem)
  590. $aRows[] = [
  591. 'label' => $this->_esc($aItem['label']),
  592. 'value' => $this->formatValue('number', $aItem[$sKey]),
  593. 'share' => $aItem['share'],
  594. 'bar' => round(100 * $aItem[$sKey] / $iMax, 1),
  595. ];
  596. return $this->parseHtmlByName('bars.html', [
  597. 'bx_if:items' => [
  598. 'condition' => !empty($aRows),
  599. 'content' => ['bx_repeat:items' => $aRows],
  600. ],
  601. 'bx_if:empty' => [
  602. 'condition' => empty($aRows),
  603. 'content' => ['empty' => _t('_bx_analytics_txt_no_data')],
  604. ],
  605. ]);
  606. }
  607. protected function _getJourneysHtml($aData, $oFilter)
  608. {
  609. $aEntries = [];
  610. foreach($aData['entries'] as $aEntry)
  611. $aEntries[] = [
  612. 'page' => $this->_esc($aEntry['page']),
  613. 'label' => $this->_esc($aEntry['page']),
  614. 'sessions' => $this->formatValue('number', $aEntry['sessions']),
  615. 'active' => $aEntry['active'] ? 'bx-analytics-row-btn-active' : '',
  616. 'pressed' => $aEntry['active'] ? 'true' : 'false',
  617. ];
  618. $fNodes = function($aItems, $bOpen) use ($aData) {
  619. $aResult = [];
  620. foreach($aItems as $aItem) {
  621. $bExit = !empty($aItem['exit']);
  622. $bActive = !$bExit && $bOpen && $aItem['page'] === $aData['via'];
  623. $aResult[] = [
  624. 'label' => $bExit ? _t('_bx_analytics_txt_left_site') : $this->_esc($aItem['page']),
  625. 'share' => $aItem['share'],
  626. 'class' => ($bExit ? 'bx-analytics-flow-exit' : '') . ($bActive ? ' bx-analytics-flow-node-active' : ''),
  627. 'attrs' => $bOpen && !$bExit ? 'data-bx-analytics-open="via" data-bx-analytics-value="' . $this->_esc($aItem['page']) . '" aria-pressed="' . ($bActive ? 'true' : 'false') . '"' : 'disabled',
  628. ];
  629. }
  630. return $aResult;
  631. };
  632. $iMax = 1;
  633. foreach($aData['paths'] as $aPath)
  634. $iMax = max($iMax, $aPath['sessions']);
  635. $aPathRows = [];
  636. foreach($aData['paths'] as $aPath)
  637. $aPathRows[] = [
  638. 'label' => implode(' → ', $aPath['pages']),
  639. 'bar' => round(100 * $aPath['sessions'] / $iMax, 1),
  640. 'values' => ['sessions' => $aPath['sessions'], 'share' => $aPath['share']],
  641. ];
  642. return $this->parseHtmlByName('journeys.html', [
  643. 'entries_title' => _t('_bx_analytics_txt_start_page'),
  644. 'bx_if:entries' => [
  645. 'condition' => !empty($aEntries),
  646. 'content' => ['bx_repeat:entries' => $aEntries],
  647. ],
  648. 'bx_if:no_entries' => [
  649. 'condition' => empty($aEntries),
  650. 'content' => ['empty' => _t('_bx_analytics_txt_no_data')],
  651. ],
  652. 'bx_if:flow' => [
  653. 'condition' => $aData['jfrom'] !== '',
  654. 'content' => [
  655. 'flow_label' => $this->_esc(_t('_bx_analytics_txt_journey_from', $aData['jfrom'])),
  656. 'step1' => _t('_bx_analytics_txt_step', 1),
  657. 'step2' => _t('_bx_analytics_txt_step', 2),
  658. 'step3' => _t('_bx_analytics_txt_step', 3),
  659. 'from' => $this->_esc($aData['jfrom']),
  660. 'from_sessions' => $this->formatValue('number', $aData['from_sessions']),
  661. 'bx_repeat:next' => $fNodes($aData['next'], true),
  662. 'bx_repeat:after' => $fNodes($aData['after'], false),
  663. 'bx_if:no_after' => [
  664. 'condition' => empty($aData['after']),
  665. 'content' => ['no_after' => _t('_bx_analytics_txt_pick_next')],
  666. ],
  667. ],
  668. ],
  669. 'paths_title' => _t('_bx_analytics_txt_top_paths'),
  670. 'paths' => $this->_getTableHtml([
  671. 'name' => 'paths',
  672. 'caption' => _t('_bx_analytics_txt_top_paths'),
  673. 'columns' => [
  674. ['key' => 'label', 'type' => 'label', 'title' => _t('_bx_analytics_txt_path')],
  675. ['key' => 'sessions', 'type' => 'number', 'title' => _t('_bx_analytics_metric_sessions')],
  676. ['key' => 'share', 'type' => 'percent', 'title' => _t('_bx_analytics_txt_share')],
  677. ],
  678. 'rows' => $aPathRows,
  679. ], $oFilter),
  680. 'bx_if:truncated' => [
  681. 'condition' => $aData['truncated'],
  682. 'content' => ['truncated' => _t('_bx_analytics_txt_journeys_truncated')],
  683. ],
  684. ]);
  685. }
  686. protected function _getRetentionHtml($aData, $oFilter)
  687. {
  688. $aWeeks = [];
  689. for($i = 0; $i < $aData['weeks']; $i++)
  690. $aWeeks[] = ['week' => _t('_bx_analytics_txt_week_n', $i)];
  691. $oTz = new DateTimeZone($aData['tz']);
  692. $aRows = [];
  693. foreach($aData['rows'] as $aRow) {
  694. $aCells = [];
  695. foreach($aRow['weeks'] as $mixedPercent)
  696. $aCells[] = [
  697. 'heat' => $mixedPercent !== null ? round($mixedPercent / 100, 3) : 0,
  698. 'strong' => $mixedPercent !== null && $mixedPercent >= 45 ? 'bx-analytics-cohort-strong' : '',
  699. 'percent' => $mixedPercent !== null && $aRow['size'] ? $this->formatValue('percent', $mixedPercent) : '',
  700. ];
  701. $aRows[] = [
  702. 'start' => (new DateTime('@' . $aRow['start']))->setTimezone($oTz)->format('Y-m-d'),
  703. 'size' => $this->formatValue('number', $aRow['size']),
  704. 'bx_repeat:cells' => $aCells,
  705. ];
  706. }
  707. return $this->parseHtmlByName('retention.html', [
  708. 'tabs' => $this->_getTabsHtml('retention', $aData['tabs']),
  709. 'bx_if:cookieless' => [
  710. 'condition' => $aData['cookieless'],
  711. 'content' => ['cookieless' => _t('_bx_analytics_txt_retention_cookieless')],
  712. ],
  713. 'caption' => $this->_esc(_t('_bx_analytics_lmi_cpt_retention')),
  714. 'cohort_title' => _t('_bx_analytics_txt_cohort'),
  715. 'size_title' => _t('_bx_analytics_metric_visitors'),
  716. 'bx_repeat:weeks' => $aWeeks,
  717. 'bx_repeat:rows' => $aRows,
  718. ]);
  719. }
  720. protected function _getGoalsHtml($aData, $oFilter)
  721. {
  722. $sCsrf = $aData['manage'] ? (string)BxDolForm::getCsrfToken() : '';
  723. $aTypes = ['page' => _t('_bx_analytics_txt_goal_type_page'), 'event' => _t('_bx_analytics_txt_goal_type_event')];
  724. $fMax = 0;
  725. foreach($aData['goals'] as $aGoal)
  726. $fMax = max($fMax, $aGoal['rate']);
  727. $aGoals = [];
  728. $aEdit = null;
  729. foreach($aData['goals'] as $aGoal) {
  730. if($aData['edit'] !== '' && $aGoal['id'] === (int)$aData['edit'])
  731. $aEdit = $aGoal;
  732. $aGoals[] = [
  733. 'id' => $aGoal['id'],
  734. 'name' => $this->_esc($aGoal['name']),
  735. 'type' => _t('_bx_analytics_txt_goal_badge_' . $aGoal['type']),
  736. 'pattern' => $this->_esc($aGoal['pattern']),
  737. 'rate' => $this->formatValue('percent', $aGoal['rate']),
  738. 'change' => $this->_formatChange($aGoal['change']),
  739. 'trend' => $this->_trend('', $aGoal['change']),
  740. 'conversions' => _t('_bx_analytics_txt_conversions', $this->formatValue('number', $aGoal['conversions']), $this->formatValue('number', $aGoal['visitors'])),
  741. 'bar' => $fMax ? round(100 * $aGoal['rate'] / $fMax, 1) : 0,
  742. 'bx_if:actions' => [
  743. 'condition' => $aData['manage'],
  744. 'content' => [
  745. 'id' => $aGoal['id'],
  746. 'edit' => _t('_bx_analytics_txt_edit'),
  747. 'delete' => _t('_bx_analytics_txt_delete'),
  748. 'confirm' => $this->_esc(_t('_bx_analytics_txt_goal_delete_confirm', $aGoal['name'])),
  749. 'csrf' => $this->_esc($sCsrf),
  750. ],
  751. ],
  752. ];
  753. }
  754. $bForm = $aData['manage'] && ($aEdit || $aData['edit'] === 'new');
  755. return $this->parseHtmlByName('goals.html', [
  756. 'bx_if:goals' => [
  757. 'condition' => !empty($aGoals),
  758. 'content' => ['bx_repeat:goals' => $aGoals],
  759. ],
  760. 'bx_if:empty' => [
  761. 'condition' => empty($aGoals),
  762. 'content' => ['empty' => _t('_bx_analytics_txt_no_goals')],
  763. ],
  764. 'bx_if:add' => [
  765. 'condition' => $aData['manage'] && !$bForm,
  766. 'content' => ['add' => _t('_bx_analytics_txt_add_goal')],
  767. ],
  768. 'bx_if:form' => [
  769. 'condition' => $bForm,
  770. 'content' => [
  771. 'form_title' => _t($aEdit ? '_bx_analytics_txt_edit_goal' : '_bx_analytics_txt_add_goal'),
  772. 'form_id' => $aEdit ? $aEdit['id'] : 0,
  773. 'form_csrf' => $this->_esc($sCsrf),
  774. 'form_name' => $aEdit ? $this->_esc($aEdit['name']) : '',
  775. 'form_pattern' => $aEdit ? $this->_esc($aEdit['pattern']) : '',
  776. 'page_selected' => !$aEdit || $aEdit['type'] == 'page' ? 'selected' : '',
  777. 'event_selected' => $aEdit && $aEdit['type'] == 'event' ? 'selected' : '',
  778. 'label_name' => _t('_bx_analytics_txt_name'),
  779. 'label_type' => _t('_bx_analytics_txt_goal_type'),
  780. 'label_pattern' => _t('_bx_analytics_txt_goal_match'),
  781. 'type_page' => $aTypes['page'],
  782. 'type_event' => $aTypes['event'],
  783. 'hint' => _t('_bx_analytics_txt_match_hint'),
  784. 'save' => _t('_bx_analytics_txt_save'),
  785. 'cancel' => _t('_bx_analytics_txt_cancel'),
  786. ],
  787. ],
  788. ]);
  789. }
  790. protected function _getFunnelsHtml($aData, $oFilter)
  791. {
  792. $sCsrf = $aData['manage'] ? (string)BxDolForm::getCsrfToken() : '';
  793. $aTypes = ['page' => _t('_bx_analytics_txt_goal_type_page'), 'event' => _t('_bx_analytics_txt_goal_type_event')];
  794. $aList = [];
  795. foreach($aData['funnels'] as $aFunnel)
  796. $aList[] = [
  797. 'id' => $aFunnel['id'],
  798. 'name' => $this->_esc($aFunnel['name']),
  799. 'active' => $aFunnel['active'] ? 'bx-analytics-chip-active' : '',
  800. 'pressed' => $aFunnel['active'] ? 'true' : 'false',
  801. ];
  802. $aSteps = [];
  803. foreach($aData['steps'] as $i => $aStep)
  804. $aSteps[] = [
  805. 'number' => $i + 1,
  806. 'pattern' => $this->_esc($aStep['pattern']),
  807. 'type' => _t('_bx_analytics_txt_goal_badge_' . $aStep['type']),
  808. 'rate_bar' => $aStep['rate'],
  809. 'sessions' => _t('_bx_analytics_txt_n_sessions', $this->formatValue('number', $aStep['sessions'])),
  810. 'rate' => $this->formatValue('percent', $aStep['rate']),
  811. 'bx_if:drop' => [
  812. 'condition' => $aStep['drop'] !== null,
  813. 'content' => ['drop' => _t('_bx_analytics_txt_dropped', $this->formatValue('percent', (float)$aStep['drop']))],
  814. ],
  815. ];
  816. $sEdit = (string)$aData['edit'];
  817. $bNew = $sEdit === 'new';
  818. $bEdit = $aData['funnel'] && $sEdit !== '' && (int)$sEdit === $aData['funnel']['id'];
  819. $bForm = $aData['manage'] && ($bNew || $bEdit);
  820. $aRows = [];
  821. $aDefinition = $bEdit ? $aData['definition'] : [];
  822. for($i = 0; $i < $aData['max_steps']; $i++) {
  823. $aStep = isset($aDefinition[$i]) ? $aDefinition[$i] : ['type' => 'page', 'pattern' => ''];
  824. $aRows[] = [
  825. 'row_number' => $i + 1,
  826. 'row_type_label' => _t('_bx_analytics_txt_step_type', $i + 1),
  827. 'row_pattern_label' => _t('_bx_analytics_txt_step_match', $i + 1),
  828. 'row_page' => $aStep['type'] == 'page' ? 'selected' : '',
  829. 'row_type_page' => $aTypes['page'],
  830. 'row_type_event' => $aTypes['event'],
  831. 'row_event' => $aStep['type'] == 'event' ? 'selected' : '',
  832. 'row_pattern' => $this->_esc($aStep['pattern']),
  833. ];
  834. }
  835. return $this->parseHtmlByName('funnels.html', [
  836. 'bx_if:list' => [
  837. 'condition' => count($aList) > 1,
  838. 'content' => [
  839. 'list_label' => $this->_esc(_t('_bx_analytics_lmi_cpt_funnels')),
  840. 'bx_repeat:list' => $aList,
  841. ],
  842. ],
  843. 'bx_if:manage' => [
  844. 'condition' => $aData['manage'] && !$bForm,
  845. 'content' => [
  846. 'add' => _t('_bx_analytics_txt_add_funnel'),
  847. 'bx_if:current' => [
  848. 'condition' => (bool)$aData['funnel'],
  849. 'content' => [
  850. 'current_id' => $aData['funnel'] ? $aData['funnel']['id'] : 0,
  851. 'edit' => _t('_bx_analytics_txt_edit'),
  852. 'delete' => _t('_bx_analytics_txt_delete'),
  853. 'confirm' => $this->_esc(_t('_bx_analytics_txt_funnel_delete_confirm', $aData['funnel'] ? $aData['funnel']['name'] : '')),
  854. 'csrf' => $this->_esc($sCsrf),
  855. ],
  856. ],
  857. ],
  858. ],
  859. 'bx_if:steps' => [
  860. 'condition' => !empty($aSteps) && !$bForm,
  861. 'content' => ['bx_repeat:steps' => $aSteps],
  862. ],
  863. 'bx_if:empty' => [
  864. 'condition' => empty($aSteps) && !$bForm,
  865. 'content' => ['empty' => _t('_bx_analytics_txt_no_funnels')],
  866. ],
  867. 'bx_if:form' => [
  868. 'condition' => $bForm,
  869. 'content' => [
  870. 'form_title' => _t($bEdit ? '_bx_analytics_txt_edit_funnel' : '_bx_analytics_txt_add_funnel'),
  871. 'form_id' => $bEdit ? $aData['funnel']['id'] : 0,
  872. 'form_csrf' => $this->_esc($sCsrf),
  873. 'form_name' => $bEdit ? $this->_esc($aData['funnel']['name']) : '',
  874. 'label_name' => _t('_bx_analytics_txt_name'),
  875. 'bx_repeat:rows' => $aRows,
  876. 'hint' => _t('_bx_analytics_txt_funnel_hint'),
  877. 'save' => _t('_bx_analytics_txt_save'),
  878. 'cancel' => _t('_bx_analytics_txt_cancel'),
  879. ],
  880. ],
  881. ]);
  882. }
  883. protected function _getRevenueHtml($aData, $oFilter)
  884. {
  885. return $this->parseHtmlByName('revenue.html', [
  886. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  887. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_revenue')),
  888. 'chart_json' => $this->_json($aData['chart']),
  889. 'split' => $this->_getTableHtml($aData['split'], $oFilter),
  890. 'notes' => $this->_getNotesHtml($aData['notes']),
  891. 'import' => $this->_getImportHtml($aData['import']),
  892. ]);
  893. }
  894. protected function _getSubscriptionsHtml($aData, $oFilter)
  895. {
  896. return $this->parseHtmlByName('subscriptions.html', [
  897. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  898. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_mrr')),
  899. 'chart_json' => $this->_json($aData['chart']),
  900. 'movement_title' => _t('_bx_analytics_txt_sbs_movement'),
  901. 'movement_label' => $this->_esc(_t('_bx_analytics_txt_sbs_movement')),
  902. 'movement_json' => $this->_json($aData['movement']),
  903. 'plans_title' => $this->_esc($aData['plans']['caption']),
  904. 'plans' => $this->_getTableHtml($aData['plans'], $oFilter),
  905. 'reasons_title' => $this->_esc($aData['reasons']['caption']),
  906. 'reasons' => $this->_getTableHtml($aData['reasons'], $oFilter),
  907. 'notes' => $this->_getNotesHtml($aData['notes']),
  908. ]);
  909. }
  910. protected function _getSalesHtml($aData, $oFilter)
  911. {
  912. return $this->parseHtmlByName('sales.html', [
  913. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  914. 'items_title' => $this->_esc($aData['items']['caption']),
  915. 'items' => $this->_getTableHtml($aData['items'], $oFilter),
  916. 'sellers_title' => $this->_esc($aData['sellers']['caption']),
  917. 'sellers' => $this->_getTableHtml($aData['sellers'], $oFilter),
  918. 'bx_if:invoices' => [
  919. 'condition' => !empty($aData['invoices']),
  920. 'content' => [
  921. 'invoices_title' => _t('_bx_analytics_txt_sales_invoices'),
  922. 'invoices' => !empty($aData['invoices']) ? $this->_getTilesHtml($aData['invoices']) : '',
  923. ],
  924. ],
  925. 'notes' => $this->_getNotesHtml($aData['notes']),
  926. ]);
  927. }
  928. protected function _getCreditsHtml($aData, $oFilter)
  929. {
  930. if(empty($aData['available']))
  931. return $this->parseHtmlByName('credits.html', [
  932. 'bx_if:credits' => ['condition' => false, 'content' => []],
  933. 'bx_if:none' => [
  934. 'condition' => true,
  935. 'content' => ['empty' => _t('_bx_analytics_txt_credits_none')],
  936. ],
  937. 'notes' => '',
  938. ]);
  939. return $this->parseHtmlByName('credits.html', [
  940. 'bx_if:credits' => [
  941. 'condition' => true,
  942. 'content' => [
  943. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  944. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_credits')),
  945. 'chart_json' => $this->_json($aData['chart']),
  946. 'spenders_title' => $this->_esc($aData['spenders']['caption']),
  947. 'spenders' => $this->_getTableHtml($aData['spenders'], $oFilter),
  948. 'spent_title' => $this->_esc($aData['spent']['caption']),
  949. 'spent' => $this->_getTableHtml($aData['spent'], $oFilter),
  950. ],
  951. ],
  952. 'bx_if:none' => ['condition' => false, 'content' => []],
  953. 'notes' => $this->_getNotesHtml($aData['notes']),
  954. ]);
  955. }
  956. /**
  957. * Errors: the list with its key figures, or one error group. Messages, sources and
  958. * stacks come from visitors' browsers and apps: every one is escaped.
  959. */
  960. protected function _getErrorsHtml($aData, $oFilter)
  961. {
  962. if($aData['view'] == 'list')
  963. return $this->parseHtmlByName('errors.html', [
  964. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  965. 'table' => $this->_getTableHtml($aData, $oFilter),
  966. ]);
  967. if(!empty($aData['missing']))
  968. return $this->parseHtmlByName('error_missing.html', [
  969. 'back' => _t('_bx_analytics_txt_back_errors'),
  970. 'text' => $this->_esc($aData['title']),
  971. ]);
  972. $sCsrf = $aData['manage'] ? (string)BxDolForm::getCsrfToken() : '';
  973. $aActions = [];
  974. foreach($aData['actions'] as $aAction)
  975. $aActions[] = [
  976. 'action_op' => $aAction['op'],
  977. 'action_id' => (int)$aData['id'],
  978. 'action_csrf' => $this->_esc($sCsrf),
  979. 'action_title' => $aAction['title'],
  980. ];
  981. $aFacts = [];
  982. foreach($aData['facts'] as $aFact)
  983. $aFacts[] = [
  984. 'fact_title' => $aFact['title'],
  985. 'fact_value' => $this->formatValue($aFact['type'], $aFact['value']),
  986. ];
  987. $aBreakdowns = [];
  988. foreach($aData['breakdowns'] as $aTable)
  989. $aBreakdowns[] = [
  990. 'breakdown_title' => $this->_esc($aTable['caption']),
  991. 'breakdown' => $this->_getTableHtml($aTable, $oFilter),
  992. ];
  993. $bStack = $aData['stack'] !== '';
  994. return $this->parseHtmlByName('error.html', [
  995. 'back' => _t('_bx_analytics_txt_back_errors'),
  996. 'title' => $this->_esc($aData['title']),
  997. 'bx_if:actions' => [
  998. 'condition' => !empty($aActions),
  999. 'content' => [
  1000. 'actions_label' => $this->_esc(_t('_bx_analytics_txt_error_actions')),
  1001. 'bx_repeat:actions' => $aActions,
  1002. ],
  1003. ],
  1004. 'bx_repeat:facts' => $aFacts,
  1005. 'tiles' => $this->_getTilesHtml($aData['tiles']),
  1006. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_error', $aData['title'])),
  1007. 'chart_json' => $this->_json($aData['chart']),
  1008. 'stack_title' => _t('_bx_analytics_txt_error_stack'),
  1009. 'bx_if:no_stack' => [
  1010. 'condition' => !$bStack,
  1011. 'content' => ['no_stack' => _t($aData['stack_hidden'] ? '_bx_analytics_txt_error_stack_hidden' : '_bx_analytics_txt_error_no_stack')],
  1012. ],
  1013. 'bx_if:breakdowns' => [
  1014. 'condition' => !empty($aBreakdowns),
  1015. 'content' => ['bx_repeat:breakdowns' => $aBreakdowns],
  1016. ],
  1017. 'latest_title' => $this->_esc($aData['occurrences']['caption']),
  1018. 'latest' => $this->_getTableHtml($aData['occurrences'], $oFilter),
  1019. // last: visitor text goes in after every placeholder is filled
  1020. 'bx_if:stack' => [
  1021. 'condition' => $bStack,
  1022. 'content' => [
  1023. 'stack_label' => $this->_esc(_t('_bx_analytics_txt_error_stack')),
  1024. 'stack' => $this->_esc($aData['stack']),
  1025. ],
  1026. ],
  1027. ]);
  1028. }
  1029. /**
  1030. * Performance: a card per metric (75th percentile, rating, distribution) that picks the
  1031. * metric for the chart and the tables.
  1032. */
  1033. protected function _getPerformanceHtml($aData, $oFilter)
  1034. {
  1035. $aDevices = [];
  1036. foreach($aData['devices'] as $aDevice)
  1037. $aDevices[] = [
  1038. 'device_value' => $aDevice['value'],
  1039. 'device_title' => $aDevice['title'],
  1040. 'device_selected' => $aDevice['selected'] ? 'selected="selected"' : '',
  1041. ];
  1042. $aCards = [];
  1043. foreach($aData['cards'] as $aCard)
  1044. $aCards[] = [
  1045. 'card_metric' => $aCard['metric'],
  1046. 'card_title' => $aCard['title'],
  1047. 'card_value' => $aCard['value'] !== null ? $this->formatValue($aCard['unit'], $aCard['value']) : _t('_bx_analytics_txt_not_enough_data'),
  1048. 'card_rating' => $aCard['rating'] !== '' ? str_replace('_', '-', $aCard['rating']) : 'none',
  1049. 'card_rating_title' => $aCard['rating'] !== '' ? _t('_bx_analytics_rating_' . $aCard['rating']) : '',
  1050. 'card_pressed' => $aCard['active'] ? 'true' : 'false',
  1051. 'card_good' => $aCard['good'],
  1052. 'card_ni' => $aCard['needs_improvement'],
  1053. 'card_poor' => $aCard['poor'],
  1054. 'card_dist' => $aCard['samples'] ? _t('_bx_analytics_txt_vital_distribution', $this->formatValue('percent', $aCard['good']), $this->formatValue('percent', $aCard['needs_improvement']), $this->formatValue('percent', $aCard['poor'])) : '',
  1055. 'card_samples' => _t('_bx_analytics_txt_n_samples', $this->formatValue('number', $aCard['samples'])),
  1056. 'card_thresholds' => _t('_bx_analytics_txt_vital_thresholds', $this->formatValue($aCard['unit'], $aCard['good_max']), $this->formatValue($aCard['unit'], $aCard['poor_min'])),
  1057. ];
  1058. return $this->parseHtmlByName('performance.html', [
  1059. 'device_label' => _t('_bx_analytics_txt_device'),
  1060. 'bx_repeat:devices' => $aDevices,
  1061. 'cards_label' => $this->_esc(_t('_bx_analytics_txt_vitals_metrics')),
  1062. 'bx_repeat:cards' => $aCards,
  1063. 'chart_title' => $this->_esc(_t('_bx_analytics_txt_chart_vital', $aData['metric_title'])),
  1064. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_vital', $aData['metric_title'])),
  1065. 'chart_json' => $this->_json($aData['chart']),
  1066. 'platforms_title' => $this->_esc($aData['platforms']['caption']),
  1067. 'platforms' => $this->_getTableHtml($aData['platforms'], $oFilter),
  1068. 'pages_title' => $this->_esc($aData['pages']['caption']),
  1069. 'pages' => $this->_getTableHtml($aData['pages'], $oFilter),
  1070. ]);
  1071. }
  1072. /**
  1073. * A sparkline of values over time, scaled to its own maximum.
  1074. */
  1075. protected function _getSparkHtml($aValues)
  1076. {
  1077. $aValues = array_values($aValues);
  1078. $iCount = count($aValues);
  1079. if(!$iCount)
  1080. return '';
  1081. $fMax = max(1, max($aValues));
  1082. $aPoints = [];
  1083. foreach($aValues as $i => $mixedValue)
  1084. $aPoints[] = round($iCount > 1 ? 100 * $i / ($iCount - 1) : 50, 2) . ',' . round(22 - 20 * (float)$mixedValue / $fMax, 2);
  1085. if($iCount == 1)
  1086. $aPoints[] = '100,' . round(22 - 20 * (float)$aValues[0] / $fMax, 2);
  1087. return $this->parseHtmlByName('spark.html', [
  1088. 'points' => implode(' ', $aPoints),
  1089. ]);
  1090. }
  1091. /**
  1092. * Short notes under a report: how amounts are counted, what is missing.
  1093. */
  1094. protected function _getNotesHtml($aNotes)
  1095. {
  1096. if(empty($aNotes))
  1097. return '';
  1098. $aItems = [];
  1099. foreach($aNotes as $sNote)
  1100. $aItems[] = ['note' => $this->_esc($sNote)];
  1101. return $this->parseHtmlByName('notes.html', [
  1102. 'bx_repeat:notes' => $aItems,
  1103. ]);
  1104. }
  1105. /**
  1106. * The import of past payments, with a button to run a step now, for administrators.
  1107. */
  1108. protected function _getImportHtml($aImport)
  1109. {
  1110. if(empty($aImport) || empty($aImport['state']))
  1111. return '';
  1112. $bRunning = $aImport['state'] == 'running';
  1113. return $this->parseHtmlByName('import.html', [
  1114. 'title' => _t('_bx_analytics_txt_import_title'),
  1115. 'text' => $this->_esc(_t($bRunning ? '_bx_analytics_txt_import_running' : '_bx_analytics_txt_import_done', $this->formatValue('number', $aImport['imported']))),
  1116. 'hint' => _t('_bx_analytics_txt_import_hint'),
  1117. 'button' => _t($bRunning ? '_bx_analytics_txt_import_continue' : '_bx_analytics_txt_import_again'),
  1118. 'op' => $bRunning ? 'import' : 'restart',
  1119. 'confirm' => $bRunning ? '' : $this->_esc(_t('_bx_analytics_txt_import_confirm')),
  1120. 'csrf' => $this->_esc((string)BxDolForm::getCsrfToken()),
  1121. ]);
  1122. }
  1123. /**
  1124. * Key figures that are not chart switches: title, value and an optional note or change.
  1125. */
  1126. protected function _getTilesHtml($aTiles)
  1127. {
  1128. $aItems = [];
  1129. foreach($aTiles as $aTile)
  1130. $aItems[] = [
  1131. 'title' => $aTile['title'],
  1132. // nothing to measure (a rate without a base): say so instead of an empty tile
  1133. 'value' => $aTile['value'] === null ? _t('_bx_analytics_txt_not_available') : $this->formatValue(isset($aTile['type']) ? $aTile['type'] : 'number', $aTile['value']),
  1134. 'bx_if:sub' => [
  1135. 'condition' => !empty($aTile['sub']),
  1136. 'content' => ['sub' => !empty($aTile['sub']) ? $this->_esc($aTile['sub']) : ''],
  1137. ],
  1138. 'bx_if:change' => [
  1139. 'condition' => isset($aTile['change']) && $aTile['change'] !== null,
  1140. 'content' => [
  1141. 'change' => isset($aTile['change']) ? $this->_formatChange($aTile['change']) : '',
  1142. 'trend' => isset($aTile['change']) ? $this->_trend(isset($aTile['metric']) ? $aTile['metric'] : '', $aTile['change']) : 'flat',
  1143. ],
  1144. ],
  1145. ];
  1146. return $this->parseHtmlByName('tiles.html', [
  1147. 'bx_repeat:tiles' => $aItems,
  1148. ]);
  1149. }
  1150. /**
  1151. * Query: what to break down and sort by, the first values over time, the table, and the
  1152. * CSV download of every value.
  1153. */
  1154. protected function _getQueryHtml($aData, $oFilter)
  1155. {
  1156. $aDimensions = ['event' => [], 'session' => []];
  1157. foreach($aData['dimensions'] as $aDimension)
  1158. $aDimensions[$aDimension['kind']][] = [
  1159. 'dim_value' => $aDimension['name'],
  1160. 'dim_title' => $aDimension['title'],
  1161. 'dim_selected' => $aDimension['name'] == $aData['dimension'] ? 'selected="selected"' : '',
  1162. ];
  1163. $aMetrics = [];
  1164. foreach($aData['metrics'] as $aMetric)
  1165. $aMetrics[] = [
  1166. 'metric_value' => $aMetric['name'],
  1167. 'metric_title' => $aMetric['title'],
  1168. 'metric_selected' => $aMetric['name'] == $aData['metric'] ? 'selected="selected"' : '',
  1169. ];
  1170. $aDirs = [];
  1171. foreach(['desc', 'asc'] as $sDir)
  1172. $aDirs[] = [
  1173. 'dir_value' => $sDir,
  1174. 'dir_title' => _t('_bx_analytics_txt_sort_' . $sDir),
  1175. 'dir_selected' => $sDir == $aData['dir'] ? 'selected="selected"' : '',
  1176. ];
  1177. $bRows = !empty($aData['rows']);
  1178. $sExportUrl = bx_append_url_params(BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'export/query/', http_build_query(array_merge($oFilter->getParams(), [
  1179. 'q_dim' => $aData['dimension'],
  1180. 'q_metric' => $aData['metric'],
  1181. 'q_dir' => $aData['dir'],
  1182. ]), '', '&', PHP_QUERY_RFC3986));
  1183. return $this->parseHtmlByName('query.html', [
  1184. 'dimension_label' => _t('_bx_analytics_txt_dimension'),
  1185. 'event_dims_label' => _t('_bx_analytics_txt_dims_event'),
  1186. 'bx_repeat:event_dims' => $aDimensions['event'],
  1187. 'session_dims_label' => _t('_bx_analytics_txt_dims_session'),
  1188. 'bx_repeat:session_dims' => $aDimensions['session'],
  1189. 'metric_label' => _t('_bx_analytics_txt_metric'),
  1190. 'bx_repeat:metrics' => $aMetrics,
  1191. 'dir_label' => _t('_bx_analytics_txt_sort'),
  1192. 'bx_repeat:dirs' => $aDirs,
  1193. 'bx_if:download' => [
  1194. 'condition' => $bRows,
  1195. 'content' => [
  1196. 'export_url' => $this->_esc($sExportUrl),
  1197. 'download' => _t('_bx_analytics_txt_download_csv'),
  1198. ],
  1199. ],
  1200. 'bx_if:chart' => [
  1201. 'condition' => $bRows,
  1202. 'content' => [
  1203. 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_query', _t('_bx_analytics_metric_' . $aData['metric']), count($aData['chart']['series']))),
  1204. 'chart_json' => $this->_json($aData['chart']),
  1205. ],
  1206. ],
  1207. 'table' => $this->_getTableHtml($aData, $oFilter),
  1208. 'bx_if:truncated' => [
  1209. 'condition' => $aData['total'] > $aData['export_max'],
  1210. 'content' => [
  1211. 'truncated' => _t('_bx_analytics_txt_csv_truncated', $this->formatValue('number', $aData['export_max']), $this->formatValue('number', $aData['total'])),
  1212. ],
  1213. ],
  1214. ]);
  1215. }
  1216. /**
  1217. * A table report: columns with types, rows with a label cell that filters, links or is plain.
  1218. */
  1219. protected function _getTableHtml($aTable, $oFilter)
  1220. {
  1221. $aColumns = [];
  1222. foreach($aTable['columns'] as $aColumn)
  1223. $aColumns[] = [
  1224. 'type' => $aColumn['type'],
  1225. 'title' => $aColumn['title'],
  1226. // the column the rows are sorted by: ascending or descending
  1227. 'sort' => isset($aColumn['sort']) && in_array($aColumn['sort'], ['ascending', 'descending'], true) ? 'aria-sort="' . $aColumn['sort'] . '"' : '',
  1228. ];
  1229. $aRows = [];
  1230. foreach($aTable['rows'] as $aRow) {
  1231. $aCells = [];
  1232. foreach($aTable['columns'] as $aColumn) {
  1233. $sKey = $aColumn['key'];
  1234. $bLabel = $aColumn['type'] == 'label';
  1235. $aCells[] = [
  1236. 'type' => $aColumn['type'],
  1237. 'bx_if:label' => [
  1238. 'condition' => $bLabel,
  1239. 'content' => $bLabel ? $this->_getLabelCell($aRow) : [],
  1240. ],
  1241. 'bx_if:value' => [
  1242. 'condition' => !$bLabel,
  1243. 'content' => [
  1244. 'cell_value' => !$bLabel ? $this->formatValue($aColumn['type'], isset($aRow['values'][$sKey]) ? $aRow['values'][$sKey] : null) : '',
  1245. ],
  1246. ],
  1247. ];
  1248. }
  1249. $aRows[] = ['bx_repeat:cells' => $aCells];
  1250. }
  1251. $aPager = isset($aTable['pages']) && $aTable['pages'] > 1 ? [
  1252. 'name' => $aTable['name'],
  1253. 'page' => $aTable['page'],
  1254. 'pages' => $aTable['pages'],
  1255. 'prev' => max(1, $aTable['page'] - 1),
  1256. 'next' => min($aTable['pages'], $aTable['page'] + 1),
  1257. 'prev_disabled' => $aTable['page'] <= 1 ? 'disabled' : '',
  1258. 'next_disabled' => $aTable['page'] >= $aTable['pages'] ? 'disabled' : '',
  1259. 'txt_prev' => _t('_bx_analytics_txt_prev'),
  1260. 'txt_next' => _t('_bx_analytics_txt_next'),
  1261. 'txt_page' => _t('_bx_analytics_txt_page_of', $aTable['page'], $aTable['pages']),
  1262. ] : [];
  1263. return $this->parseHtmlByName('table.html', [
  1264. 'tabs' => !empty($aTable['tabs']) ? $this->_getTabsHtml($aTable['name'], $aTable['tabs']) : '',
  1265. 'panel_attrs' => !empty($aTable['tabs']) ? 'role="tabpanel" id="bx-analytics-panel-' . $aTable['name'] . '"' : '',
  1266. 'name' => $aTable['name'],
  1267. 'caption' => $this->_esc($aTable['caption']),
  1268. 'bx_if:rows' => [
  1269. 'condition' => !empty($aRows),
  1270. 'content' => [
  1271. 'caption' => $this->_esc($aTable['caption']),
  1272. 'bx_repeat:columns' => $aColumns,
  1273. 'bx_repeat:rows' => $aRows,
  1274. ],
  1275. ],
  1276. 'bx_if:empty' => [
  1277. 'condition' => empty($aRows),
  1278. 'content' => ['empty' => !empty($aTable['empty']) ? $aTable['empty'] : _t('_bx_analytics_txt_no_data')],
  1279. ],
  1280. 'bx_if:pager' => [
  1281. 'condition' => !empty($aPager),
  1282. 'content' => $aPager,
  1283. ],
  1284. ]);
  1285. }
  1286. protected function _getLabelCell($aRow)
  1287. {
  1288. $sLabel = $this->_esc($aRow['label']);
  1289. $sSub = isset($aRow['sub']) ? $this->_esc($aRow['sub']) : '';
  1290. $bFilter = !empty($aRow['filter']);
  1291. $bLink = !$bFilter && !empty($aRow['link']);
  1292. $bOpen = !$bFilter && !$bLink && !empty($aRow['open']);
  1293. return [
  1294. 'bar' => isset($aRow['bar']) ? $aRow['bar'] : 0,
  1295. // placeholder names are unique across the row: the parser fills every one with
  1296. // the same name from any part of the row
  1297. 'bx_if:filter' => [
  1298. 'condition' => $bFilter,
  1299. 'content' => [
  1300. 'filter_dim' => $bFilter ? $aRow['filter']['dim'] : '',
  1301. 'filter_value' => $bFilter ? $this->_esc($aRow['filter']['value']) : '',
  1302. 'filter_title' => $this->_esc(_t('_bx_analytics_txt_filter_by', $aRow['label'])),
  1303. 'label' => $sLabel,
  1304. ],
  1305. ],
  1306. 'bx_if:link' => [
  1307. 'condition' => $bLink,
  1308. 'content' => [
  1309. 'link_href' => $bLink ? $this->_esc($aRow['link']) : '',
  1310. 'label' => $sLabel,
  1311. ],
  1312. ],
  1313. 'bx_if:open' => [
  1314. 'condition' => $bOpen,
  1315. 'content' => [
  1316. 'open_param' => $bOpen ? $aRow['open']['param'] : '',
  1317. 'open_value' => $bOpen ? $this->_esc($aRow['open']['value']) : '',
  1318. 'label' => $sLabel,
  1319. ],
  1320. ],
  1321. 'bx_if:text' => [
  1322. 'condition' => !$bFilter && !$bLink && !$bOpen,
  1323. 'content' => ['label' => $sLabel],
  1324. ],
  1325. 'bx_if:sub' => [
  1326. 'condition' => $sSub !== '',
  1327. 'content' => ['sub' => $sSub],
  1328. ],
  1329. ];
  1330. }
  1331. protected function _getTabsHtml($sGroup, $aTabs)
  1332. {
  1333. if(count($aTabs) < 2)
  1334. return '';
  1335. $aItems = [];
  1336. foreach($aTabs as $aTab)
  1337. $aItems[] = [
  1338. 'group' => $sGroup,
  1339. 'name' => $aTab['name'],
  1340. 'title' => $aTab['title'],
  1341. 'selected' => $aTab['active'] ? 'true' : 'false',
  1342. 'tabindex' => $aTab['active'] ? '0' : '-1',
  1343. ];
  1344. return $this->parseHtmlByName('tabs.html', [
  1345. 'group' => $sGroup,
  1346. 'label' => $this->_esc(_t('_bx_analytics_group_' . $sGroup)),
  1347. 'bx_repeat:tabs' => $aItems,
  1348. ]);
  1349. }
  1350. /**
  1351. * Studio > API: pick a report, edit its parameters, see the call as bx_srv, as an
  1352. * api.php URL and as NEO's request, and run it.
  1353. *
  1354. * @param array $aData catalog (see BxAnalyticsModule::getApiCatalog), api_enabled, api_access
  1355. */
  1356. public function getStudioApi($aData)
  1357. {
  1358. $aCatalog = $aData['catalog'];
  1359. $aNotices = [];
  1360. if(!$aData['api_enabled'])
  1361. $aNotices[] = ['notice' => $this->_esc(_t('_bx_analytics_txt_api_disabled'))];
  1362. elseif(!$aData['api_access'])
  1363. $aNotices[] = ['notice' => $this->_esc(_t('_bx_analytics_txt_api_no_access'))];
  1364. // a top list first: it shows what parameters do
  1365. $sSelected = isset($aCatalog['reports']['top']) ? 'top' : (string)key($aCatalog['reports']);
  1366. $aReports = [];
  1367. $aDocs = [];
  1368. foreach($aCatalog['reports'] as $sReport => $aParams) {
  1369. $aReports[] = [
  1370. 'report_value' => $sReport,
  1371. 'report_title' => $this->_esc(_t('_bx_analytics_report_' . $sReport)),
  1372. 'report_selected' => $sReport == $sSelected ? 'selected="selected"' : '',
  1373. ];
  1374. $aDocs[] = [
  1375. 'doc_report' => $sReport,
  1376. 'doc_hidden' => $sReport == $sSelected ? '' : 'hidden',
  1377. 'doc_table' => $this->_getApiParamsHtml($aParams, _t('_bx_analytics_txt_api_report_params', _t('_bx_analytics_report_' . $sReport))),
  1378. ];
  1379. }
  1380. $this->_addToolsAssets();
  1381. return $this->_getToolsJsInit() . $this->parseHtmlByName('studio_api.html', [
  1382. 'bx_if:notices' => [
  1383. 'condition' => !empty($aNotices),
  1384. 'content' => ['bx_repeat:notices' => $aNotices],
  1385. ],
  1386. 'intro' => _t('_bx_analytics_txt_api_intro'),
  1387. 'csrf' => $this->_esc((string)BxDolForm::getCsrfToken()),
  1388. 'report_label' => _t('_bx_analytics_txt_api_report'),
  1389. 'bx_repeat:reports' => $aReports,
  1390. 'params_label' => _t('_bx_analytics_txt_api_params'),
  1391. 'params_hint' => _t('_bx_analytics_txt_api_params_hint'),
  1392. 'docs_label' => _t('_bx_analytics_txt_api_docs'),
  1393. 'params_default' => $this->_esc($sSelected == 'top' ? '{"group": "pages", "range": "7d"}' : '{"range": "7d"}'),
  1394. 'common_table' => $this->_getApiParamsHtml($aCatalog['common'], _t('_bx_analytics_txt_api_common_params')),
  1395. 'bx_repeat:docs' => $aDocs,
  1396. 'code_srv' => $this->_getCodeHtml('bx-analytics-api-srv', _t('_bx_analytics_txt_api_srv'), ''),
  1397. 'code_url' => $this->_getCodeHtml('bx-analytics-api-url', _t('_bx_analytics_txt_api_url'), ''),
  1398. 'code_neo' => $this->_getCodeHtml('bx-analytics-api-neo', _t('_bx_analytics_txt_api_neo'), ''),
  1399. 'run' => _t('_bx_analytics_txt_api_run'),
  1400. 'response_label' => _t('_bx_analytics_txt_api_response'),
  1401. 'copy' => _t('_bx_analytics_txt_copy'),
  1402. 'copied' => $this->_esc(_t('_bx_analytics_txt_copied')),
  1403. 'catalog_json' => $this->_json([
  1404. 'root' => BX_DOL_URL_ROOT,
  1405. 'module' => $this->MODULE,
  1406. 'reports' => array_keys($aCatalog['reports']),
  1407. ]),
  1408. ]);
  1409. }
  1410. /**
  1411. * A table of parameters: name and what it accepts.
  1412. */
  1413. protected function _getApiParamsHtml($aParams, $sCaption)
  1414. {
  1415. $aRows = [];
  1416. foreach($aParams as $sName => $aParam) {
  1417. switch($aParam['type']) {
  1418. case 'enum':
  1419. $sAccepts = _t('_bx_analytics_txt_api_type_enum', implode(', ', $aParam['values']));
  1420. break;
  1421. case 'enum_by':
  1422. $aLists = [];
  1423. foreach($aParam['values'] as $sBy => $aValues)
  1424. $aLists[] = $sBy . ': ' . implode(', ', $aValues);
  1425. $sAccepts = _t('_bx_analytics_txt_api_type_enum_by', $aParam['by'], implode('; ', $aLists));
  1426. break;
  1427. case 'int':
  1428. $sAccepts = isset($aParam['max']) ? _t('_bx_analytics_txt_api_type_int_range', $aParam['min'], $aParam['max']) : _t('_bx_analytics_txt_api_type_int', $aParam['min']);
  1429. break;
  1430. case 'date':
  1431. $sAccepts = _t('_bx_analytics_txt_api_type_date');
  1432. break;
  1433. case 'dims':
  1434. $sAccepts = _t('_bx_analytics_txt_api_type_dims', implode(', ', $aParam['values']));
  1435. break;
  1436. default:
  1437. $sAccepts = _t('_bx_analytics_txt_api_type_string');
  1438. }
  1439. if(isset($aParam['default']))
  1440. $sAccepts .= ' ' . _t('_bx_analytics_txt_api_default', $aParam['default']);
  1441. $aRows[] = [
  1442. 'param_name' => $this->_esc($sName),
  1443. 'param_accepts' => $this->_esc($sAccepts),
  1444. ];
  1445. }
  1446. return $this->parseHtmlByName('api_params.html', [
  1447. 'caption' => $this->_esc($sCaption),
  1448. 'bx_if:params' => [
  1449. 'condition' => !empty($aRows),
  1450. 'content' => [
  1451. 'caption' => $this->_esc($sCaption),
  1452. 'col_param' => _t('_bx_analytics_txt_api_col_param'),
  1453. 'col_accepts' => _t('_bx_analytics_txt_api_col_accepts'),
  1454. 'bx_repeat:params' => $aRows,
  1455. ],
  1456. ],
  1457. 'bx_if:none' => [
  1458. 'condition' => empty($aRows),
  1459. 'content' => ['none' => _t('_bx_analytics_txt_api_no_params')],
  1460. ],
  1461. ]);
  1462. }
  1463. /**
  1464. * Studio > Dashboards: site pages with analytics blocks, and a form for a new one.
  1465. *
  1466. * @param array $aData pages (see BxAnalyticsModule::getDashboards)
  1467. */
  1468. public function getStudioDashboards($aData)
  1469. {
  1470. $aPages = [];
  1471. foreach($aData['pages'] as $aPage)
  1472. $aPages[] = [
  1473. 'view_url' => $this->_esc($aPage['view_url']),
  1474. 'page_title' => $this->_esc($aPage['title']),
  1475. 'blocks' => $this->formatValue('number', $aPage['blocks']),
  1476. 'api_blocks' => $this->formatValue('number', $aPage['api_blocks']),
  1477. 'edit_url' => $this->_esc($aPage['edit_url']),
  1478. 'edit' => _t('_bx_analytics_txt_dash_edit'),
  1479. 'edit_label' => $this->_esc(_t('_bx_analytics_txt_dash_edit_label', $aPage['title'])),
  1480. ];
  1481. $this->_addToolsAssets();
  1482. return $this->_getToolsJsInit() . $this->parseHtmlByName('studio_dashboards.html', [
  1483. 'intro' => _t('_bx_analytics_txt_dash_intro'),
  1484. 'bx_if:pages' => [
  1485. 'condition' => !empty($aPages),
  1486. 'content' => [
  1487. 'caption' => $this->_esc(_t('_bx_analytics_txt_dash_pages')),
  1488. 'col_page' => _t('_bx_analytics_txt_dash_col_page'),
  1489. 'col_blocks' => _t('_bx_analytics_txt_dash_blocks'),
  1490. 'col_neo' => _t('_bx_analytics_txt_dash_neo_blocks'),
  1491. 'col_actions' => _t('_bx_analytics_txt_dash_col_actions'),
  1492. 'bx_repeat:pages' => $aPages,
  1493. ],
  1494. ],
  1495. 'bx_if:empty' => [
  1496. 'condition' => empty($aPages),
  1497. 'content' => ['empty' => _t('_bx_analytics_txt_dash_empty')],
  1498. ],
  1499. 'neo_note' => _t('_bx_analytics_txt_dash_neo_note'),
  1500. 'create_title' => _t('_bx_analytics_txt_dash_create'),
  1501. 'csrf' => $this->_esc((string)BxDolForm::getCsrfToken()),
  1502. 'label_title' => _t('_bx_analytics_txt_dash_title'),
  1503. 'create_hint' => _t('_bx_analytics_txt_dash_create_hint'),
  1504. 'disable_warning' => _t('_bx_analytics_txt_dash_disable_warning'),
  1505. 'create' => _t('_bx_analytics_txt_dash_create_btn'),
  1506. ]);
  1507. }
  1508. /**
  1509. * Studio > Setup: tracker status per platform, checks of the admin's own browser, what
  1510. * each client needs, and custom events.
  1511. *
  1512. * @param array $aData status (see BxAnalyticsModule::getSetupStatus), tracking, injection,
  1513. * track_actions, alerts, api_enabled, api_key, api_origin
  1514. */
  1515. public function getStudioSetup($aData)
  1516. {
  1517. $sRoot = BX_DOL_URL_ROOT;
  1518. $sCsrf = $this->_esc((string)BxDolForm::getCsrfToken());
  1519. // what NEO and the apps need from the site
  1520. $aNeoChecks = [
  1521. ['on' => $aData['api_enabled'], 'text' => _t('_bx_analytics_txt_setup_neo_api')],
  1522. ['on' => $aData['api_key'] || $aData['api_origin'], 'text' => _t('_bx_analytics_txt_setup_neo_access')],
  1523. ];
  1524. foreach([BX_ANALYTICS_PLATFORM_NEXT_WEB, BX_ANALYTICS_PLATFORM_RN_IOS, BX_ANALYTICS_PLATFORM_RN_ANDROID] as $sPlatform) {
  1525. $aStatus = $aData['status'][$sPlatform];
  1526. $aNeoChecks[] = [
  1527. 'on' => $aStatus['enabled'],
  1528. 'text' => _t($aStatus['enabled'] ? '_bx_analytics_txt_setup_platform_on' : '_bx_analytics_txt_setup_platform_off', _t($this->_oConfig->getPlatforms()[$sPlatform])),
  1529. ];
  1530. }
  1531. $aChecks = [];
  1532. foreach($aNeoChecks as $aCheck)
  1533. $aChecks[] = [
  1534. 'check_state' => $aCheck['on'] ? 'yes' : 'no',
  1535. 'check_icon' => $this->_getIconHtml($aCheck['on'] ? 'check' : 'x'),
  1536. 'check_text' => $this->_esc($aCheck['text']),
  1537. ];
  1538. $aAlerts = [];
  1539. foreach($aData['alerts'] as $sAlert)
  1540. $aAlerts[] = ['alert' => $this->_esc($sAlert)];
  1541. $sCurl = "curl -X POST '" . $sRoot . "api.php?r=" . $this->MODULE . "/collect' \\\n"
  1542. . " -H 'Authorization: Bearer YOUR_API_KEY' \\\n"
  1543. . " -H 'Content-Type: application/json' \\\n"
  1544. . " -d '{\"platform\": \"api\", \"app\": \"my-client\", \"app_version\": \"1.0.0\", \"hits\": [{\"type\": \"pageview\", \"url\": \"https://example.com/pricing\", \"title\": \"Pricing\"}, {\"type\": \"event\", \"name\": \"signup_click\", \"url\": \"https://example.com/pricing\", \"props\": {\"plan\": \"pro\"}}]}'";
  1545. $this->_addToolsAssets();
  1546. return $this->_getToolsJsInit() . $this->parseHtmlByName('studio_setup.html', [
  1547. 'bx_if:tracking_off' => [
  1548. 'condition' => !$aData['tracking'],
  1549. 'content' => [
  1550. 'tracking_off' => _t('_bx_analytics_txt_setup_tracking_off'),
  1551. 'settings_url' => $this->_esc(BX_DOL_URL_STUDIO . 'module.php?name=' . $this->MODULE . '&page=settings'),
  1552. 'settings' => _t('_bx_analytics_txt_setup_open_settings'),
  1553. ],
  1554. ],
  1555. 'status_title' => _t('_bx_analytics_txt_setup_status'),
  1556. 'status' => $this->getSetupStatusHtml($aData['status']),
  1557. 'check_title' => _t('_bx_analytics_txt_setup_check_title'),
  1558. 'check_hint' => _t('_bx_analytics_txt_setup_check_hint'),
  1559. 'csrf' => $sCsrf,
  1560. 'check' => _t('_bx_analytics_txt_setup_check'),
  1561. 'test' => _t('_bx_analytics_txt_setup_test'),
  1562. 'test_warning' => _t('_bx_analytics_txt_setup_test_warning'),
  1563. 'install_title' => _t('_bx_analytics_txt_setup_install'),
  1564. 'core_web_title' => _t('_bx_analytics_platform_core_web'),
  1565. 'core_web_text' => _t('_bx_analytics_txt_setup_core_web'),
  1566. 'injection_state' => $aData['injection'] ? 'yes' : 'no',
  1567. 'injection_icon' => $this->_getIconHtml($aData['injection'] ? 'check' : 'x'),
  1568. 'injection' => _t($aData['injection'] ? '_bx_analytics_txt_setup_injection_on' : '_bx_analytics_txt_setup_injection_off'),
  1569. 'core_web_exclusions' => _t('_bx_analytics_txt_setup_core_web_exclusions'),
  1570. 'neo_title' => _t('_bx_analytics_txt_setup_neo_title'),
  1571. 'neo_text' => _t('_bx_analytics_txt_setup_neo'),
  1572. 'bx_repeat:neo_checks' => $aChecks,
  1573. 'api_title' => _t('_bx_analytics_platform_api'),
  1574. 'api_text' => _t('_bx_analytics_txt_setup_api'),
  1575. 'code_curl' => $this->_getCodeHtml('bx-analytics-setup-curl', _t('_bx_analytics_txt_setup_curl'), $sCurl),
  1576. 'core_title' => _t('_bx_analytics_platform_core'),
  1577. 'core_text' => _t('_bx_analytics_txt_setup_core'),
  1578. 'actions_state' => $aData['track_actions'] ? 'yes' : 'no',
  1579. 'actions_icon' => $this->_getIconHtml($aData['track_actions'] ? 'check' : 'x'),
  1580. 'actions' => _t($aData['track_actions'] ? '_bx_analytics_txt_setup_actions_on' : '_bx_analytics_txt_setup_actions_off'),
  1581. 'alerts_label' => _t('_bx_analytics_txt_setup_alerts'),
  1582. 'bx_repeat:alerts' => $aAlerts,
  1583. 'events_title' => _t('_bx_analytics_txt_setup_events'),
  1584. 'events_hint' => _t('_bx_analytics_txt_setup_events_hint'),
  1585. 'code_classic' => $this->_getCodeHtml('bx-analytics-setup-js', _t('_bx_analytics_txt_setup_events_classic'), "window.bx_analytics && bx_analytics.track('signup_click', {plan: 'pro'});"),
  1586. 'code_neo' => $this->_getCodeHtml('bx-analytics-setup-neo', _t('_bx_analytics_txt_setup_events_neo'), "import { trackEvent } from 'app/lib/analytics';\n\ntrackEvent('signup_click', { plan: 'pro' });"),
  1587. 'code_server' => $this->_getCodeHtml('bx-analytics-setup-srv', _t('_bx_analytics_txt_setup_events_server'), "bx_srv('" . $this->MODULE . "', 'track', ['export_done', ['rows' => 120]]);"),
  1588. 'events_warning' => _t('_bx_analytics_txt_setup_events_warning'),
  1589. ]);
  1590. }
  1591. /**
  1592. * The tracker status table, on the page and after a test event.
  1593. */
  1594. public function getSetupStatusHtml($aStatus)
  1595. {
  1596. $aPlatforms = $this->_oConfig->getPlatforms();
  1597. $aRows = [];
  1598. foreach($aStatus as $sPlatform => $aRow) {
  1599. $bApp = $this->_oConfig->isAppPlatform($sPlatform);
  1600. $aRows[] = [
  1601. 'platform_title' => _t($aPlatforms[$sPlatform]),
  1602. 'status' => $aRow['status'],
  1603. 'status_icon' => $this->_getIconHtml($aRow['status']),
  1604. 'status_title' => _t('_bx_analytics_txt_setup_status_' . $aRow['status']),
  1605. 'last' => $aRow['last'] ? $this->formatValue('datetime', $aRow['last']) : _t('_bx_analytics_txt_setup_never'),
  1606. 'hits' => $this->formatValue('number', $aRow['hits']),
  1607. // the server's own hits keep no visits
  1608. 'visitors' => $sPlatform == BX_ANALYTICS_PLATFORM_CORE ? _t('_bx_analytics_txt_not_available') : $this->formatValue('number', $aRow['visitors']),
  1609. 'sessions' => $sPlatform == BX_ANALYTICS_PLATFORM_CORE ? _t('_bx_analytics_txt_not_available') : $this->formatValue('number', $aRow['sessions']),
  1610. 'version' => $bApp && $aRow['app_version'] !== '' ? $this->_esc($aRow['app_version']) : ($bApp ? _t('_bx_analytics_txt_setup_none') : _t('_bx_analytics_txt_not_available')),
  1611. ];
  1612. }
  1613. return $this->parseHtmlByName('setup_status.html', [
  1614. 'caption' => $this->_esc(_t('_bx_analytics_txt_setup_status')),
  1615. 'col_platform' => _t('_bx_analytics_txt_platform'),
  1616. 'col_status' => _t('_bx_analytics_txt_setup_col_status'),
  1617. 'col_last' => _t('_bx_analytics_txt_setup_last_hit'),
  1618. 'col_hits' => _t('_bx_analytics_txt_setup_hits_24h'),
  1619. 'col_visitors' => _t('_bx_analytics_txt_setup_visitors_24h'),
  1620. 'col_sessions' => _t('_bx_analytics_txt_setup_sessions_7d'),
  1621. 'col_version' => _t('_bx_analytics_txt_setup_app_version'),
  1622. 'bx_repeat:rows' => $aRows,
  1623. ]);
  1624. }
  1625. /**
  1626. * The result of "Check this browser": tracked or not and why, and what the server sees.
  1627. *
  1628. * @param array $aCheck see BxAnalyticsCollector::check
  1629. */
  1630. public function getSetupCheckHtml($aCheck)
  1631. {
  1632. $bTracked = $aCheck['reasons'] == ['ok'];
  1633. $aReasons = [];
  1634. foreach($aCheck['reasons'] as $sReason)
  1635. if($sReason != 'ok')
  1636. $aReasons[] = [
  1637. 'reason_kind' => 'no',
  1638. 'reason_icon' => $this->_getIconHtml('x'),
  1639. 'reason' => _t('_bx_analytics_txt_check_' . $sReason),
  1640. ];
  1641. foreach($aCheck['notes'] as $sNote)
  1642. $aReasons[] = [
  1643. 'reason_kind' => 'note',
  1644. 'reason_icon' => $this->_getIconHtml('info'),
  1645. 'reason' => _t('_bx_analytics_txt_check_' . $sNote),
  1646. ];
  1647. $sGeo = _t('_bx_analytics_txt_check_geo_none');
  1648. if($aCheck['geo_source'] == BX_ANALYTICS_GEO_NONE)
  1649. $sGeo = _t('_bx_analytics_txt_check_geo_off');
  1650. elseif($aCheck['country_header'] !== '')
  1651. $sGeo = _t('_bx_analytics_txt_check_geo_header', $this->_esc(str_replace('_', '-', preg_replace('/^HTTP_/', '', $aCheck['country_header']))), $this->_esc($aCheck['country']));
  1652. return $this->parseHtmlByName('setup_check.html', [
  1653. 'verdict_state' => $bTracked ? 'yes' : 'no',
  1654. 'verdict_icon' => $this->_getIconHtml($bTracked ? 'check' : 'x'),
  1655. 'verdict' => _t($bTracked ? '_bx_analytics_txt_check_tracked' : '_bx_analytics_txt_check_not_tracked'),
  1656. 'bx_if:reasons' => [
  1657. 'condition' => !empty($aReasons),
  1658. 'content' => ['bx_repeat:reasons' => $aReasons],
  1659. ],
  1660. 'ip_label' => _t('_bx_analytics_txt_check_ip'),
  1661. 'ip' => $aCheck['ip'] !== '' ? $this->_esc($aCheck['ip']) : _t('_bx_analytics_txt_not_available'),
  1662. 'remote_label' => _t('_bx_analytics_txt_check_remote_addr'),
  1663. 'remote' => $aCheck['remote_addr'] !== '' ? $this->_esc($aCheck['remote_addr']) : _t('_bx_analytics_txt_not_available'),
  1664. 'geo_label' => _t('_bx_analytics_txt_check_geo'),
  1665. 'geo' => $sGeo,
  1666. 'identity_label' => _t('_bx_analytics_adm_stg_cpt_identity'),
  1667. 'identity' => _t('_bx_analytics_identity_' . $aCheck['identity']),
  1668. ]);
  1669. }
  1670. /**
  1671. * A code snippet with a copy button. The code is text: it is escaped here.
  1672. */
  1673. protected function _getCodeHtml($sId, $sLabel, $sCode)
  1674. {
  1675. return $this->parseHtmlByName('code.html', [
  1676. 'code_id' => $sId,
  1677. 'code_label' => $this->_esc($sLabel),
  1678. 'code' => $this->_esc($sCode),
  1679. 'copy' => _t('_bx_analytics_txt_copy'),
  1680. 'copied' => $this->_esc(_t('_bx_analytics_txt_copied')),
  1681. ]);
  1682. }
  1683. /**
  1684. * A small Lucide icon next to a status text, hidden from screen readers.
  1685. *
  1686. * @param string $sName receiving, quiet, nodata, off, check, x, info
  1687. */
  1688. protected function _getIconHtml($sName)
  1689. {
  1690. $aIcons = ['receiving' => 'check', 'quiet' => 'clock', 'nodata' => 'minus', 'off' => 'power', 'check' => 'check', 'x' => 'x', 'info' => 'info'];
  1691. $sIcon = isset($aIcons[$sName]) ? $aIcons[$sName] : 'info';
  1692. $aVars = [];
  1693. foreach(array_unique(array_values($aIcons)) as $sGlyph)
  1694. $aVars['bx_if:' . $sGlyph] = ['condition' => $sGlyph == $sIcon, 'content' => []];
  1695. return $this->parseHtmlByName('icon.html', $aVars);
  1696. }
  1697. protected function _addToolsAssets()
  1698. {
  1699. $this->_addAssets(['studio_tools.js'], ['dashboard.css']);
  1700. }
  1701. protected function _getToolsJsInit()
  1702. {
  1703. return $this->getJsCode('tools', [
  1704. 'aStrings' => [
  1705. 'error' => _t('_bx_analytics_txt_error_loading'),
  1706. 'running' => _t('_bx_analytics_txt_api_running'),
  1707. 'meta' => _t('_bx_analytics_txt_api_meta', '{status}', '{ms}', '{size}'),
  1708. 'truncated' => _t('_bx_analytics_txt_api_truncated'),
  1709. 'bad_json' => _t('_bx_analytics_txt_api_bad_params'),
  1710. ],
  1711. ], [
  1712. 'mask' => "if(typeof window.{object} == 'undefined') window.{object} = new {class}({params});",
  1713. 'wrap' => true,
  1714. ]);
  1715. }
  1716. /**
  1717. * Creates the dashboard script object once per page, whatever block comes first.
  1718. */
  1719. protected function _getJsInit()
  1720. {
  1721. return $this->getJsCode('dashboard', [
  1722. 'sActionUrl' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri(),
  1723. 'aStrings' => [
  1724. 'duration_h' => _t('_bx_analytics_txt_duration_h'),
  1725. 'duration_m' => _t('_bx_analytics_txt_duration_m'),
  1726. 'duration_s' => _t('_bx_analytics_txt_duration_s'),
  1727. 'ms' => _t('_bx_analytics_txt_ms'),
  1728. 'error' => _t('_bx_analytics_txt_error_loading'),
  1729. ],
  1730. ], [
  1731. 'mask' => "if(typeof window.{object} == 'undefined') window.{object} = new {class}({params});",
  1732. 'wrap' => true,
  1733. ]);
  1734. }
  1735. protected function _addDashboardAssets($sReport = '')
  1736. {
  1737. $aJs = ['chart.umd.min.js', 'dashboard.js'];
  1738. $aCss = ['dashboard.css'];
  1739. // the map uses the Leaflet copy and tile provider the site's location maps use
  1740. if($sReport == 'map') {
  1741. $aJs = array_merge(['leaflet/leaflet.js', 'leaflet-providers/leaflet-providers.js'], $aJs);
  1742. $aCss[] = BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'leaflet/|leaflet.css';
  1743. }
  1744. $this->_addAssets($aJs, $aCss);
  1745. }
  1746. protected function _addAssets($aJs, $aCss)
  1747. {
  1748. if($this->_bStudio) {
  1749. $this->addStudioJs($aJs);
  1750. $this->addStudioCss($aCss);
  1751. return;
  1752. }
  1753. $this->addJs($aJs);
  1754. $this->addCss($aCss);
  1755. }
  1756. /**
  1757. * 950, 9.5K, 95K, 1.2M
  1758. */
  1759. protected function _formatNumber($iValue)
  1760. {
  1761. $iValue = (int)$iValue;
  1762. foreach([1000000000 => 'B', 1000000 => 'M', 1000 => 'K'] as $iSize => $sSuffix)
  1763. if($iValue >= $iSize) {
  1764. $fValue = $iValue / $iSize;
  1765. return ($fValue < 10 ? rtrim(rtrim(number_format($fValue, 1), '0'), '.') : number_format(floor($fValue))) . $sSuffix;
  1766. }
  1767. return (string)$iValue;
  1768. }
  1769. protected function _formatChange($mixedChange)
  1770. {
  1771. if($mixedChange === null)
  1772. return '';
  1773. return ($mixedChange > 0 ? '+' : '') . number_format(abs($mixedChange) < 10 ? $mixedChange : round($mixedChange), abs($mixedChange) < 10 ? 1 : 0) . '%';
  1774. }
  1775. /**
  1776. * good, bad or flat, where down is good for bounce rate.
  1777. */
  1778. protected function _trend($sMetric, $mixedChange)
  1779. {
  1780. if($mixedChange === null || $mixedChange == 0)
  1781. return 'flat';
  1782. $bUp = $mixedChange > 0;
  1783. if(in_array($sMetric, ['bounce_rate', 'errors', 'refunds', 'cancelled', 'churn'], true))
  1784. return $bUp ? 'bad' : 'good';
  1785. return $bUp ? 'good' : 'bad';
  1786. }
  1787. /**
  1788. * Visitor-supplied text (paths, titles, referrers) for HTML text and attributes.
  1789. */
  1790. protected function _esc($s)
  1791. {
  1792. return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
  1793. }
  1794. /**
  1795. * JSON safe to put inside a script element.
  1796. */
  1797. protected function _json($a)
  1798. {
  1799. return json_encode($a, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE);
  1800. }
  1801. }
  1802. /** @} */