| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991 |
- <?php defined('BX_DOL') or die('hack attempt');
- /**
- * Copyright (c) UNA, Inc - https://una.io
- * MIT License - https://opensource.org/licenses/MIT
- *
- * @defgroup Analytics Analytics
- * @ingroup UnaModules
- *
- * @{
- */
- bx_import ('BxDolModuleTemplate');
- class BxAnalyticsTemplate extends BxBaseModGeneralTemplate
- {
- /**
- * Blocks are drawn inside Studio: assets go to the Studio template.
- */
- protected $_bStudio = false;
- protected $_aRanges = ['today', 'yesterday', '24h', '7d', '30d', '90d', '12m'];
- function __construct(&$oConfig, &$oDb)
- {
- $this->MODULE = 'bx_analytics';
- parent::__construct($oConfig, $oDb);
- }
- public function setStudio($bStudio)
- {
- $this->_bStudio = (bool)$bStudio;
- }
- public function isStudio()
- {
- return $this->_bStudio;
- }
- function getCanvas()
- {
- $aTmp = $this->getModule()->getSelectedModules();
- $aModules = $aTmp[0];
- $aModulesList = $aTmp[1];
- $aModulesList2 = [];
- foreach($aModules as $sModule)
- $aModulesList2[] = ['value' => $sModule, 'title' => $aModulesList[$sModule]];
- $iDaysBefore = getParam('bx_analytics_default_interval_day');
- $sDate = date('d/m/Y', time() - $iDaysBefore * 86400) . ' - ' . date('d/m/Y');
- $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'];
- $aCss = ['main.css', BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/plugins/daterangepicker/|daterangepicker.css', BX_DIRECTORY_PATH_MODULES . 'boonex/analytics/plugins/datatables/|datatables.min.css'];
- $this->_addAssets($aJs, $aCss);
- return $this->getJsCode('analytics') . $this->parseHtmlByName('canvas.html', [
- 'bx_repeat:items' => $aModulesList2,
- 'interval' => $sDate,
- 'export_to_csv_title' => _t('_bx_analytics_txt_export_to_csv_title')
- ]);
- }
- /**
- * The classic UI tracker: a small deferred script configured by data attributes.
- */
- public function getTrackerCode()
- {
- return $this->parseHtmlByName('tracker.html', [
- 'src' => $this->_getTrackerUrl(),
- 'endpoint' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'collect',
- 'platform' => BX_ANALYTICS_PLATFORM_CORE_WEB,
- 'identity' => $this->_oConfig->getIdentityMode(),
- 'dnt' => $this->_oConfig->isRespectDnt() ? 1 : 0,
- 'errors' => $this->_oConfig->isTrackErrors() ? 1 : 0,
- // percent of page loads that measure vitals, 0 for none
- 'vitals' => $this->_oConfig->isTrackVitals() ? $this->_oConfig->getVitalsSample() : 0,
- ]);
- }
- /**
- * The tracker's URL. Every classic page loads it, so when the site minifies its scripts
- * it is minified too, once per revision, into the public cache next to the site's
- * scripts (clearing the JS cache removes it); the source file otherwise, or when the
- * cache can't be written.
- */
- protected function _getTrackerUrl()
- {
- $iRevision = (int)getParam('sys_revision');
- $sSource = 'boonex/analytics/js/tracker.js';
- $sUrl = BX_DOL_URL_MODULES . $sSource . '?v=' . $iRevision;
- if(bx_is_dbg() || getParam('sys_template_cache_js_enable') != 'on' || getParam('sys_template_cache_minify_js_enable') != 'on')
- return $sUrl;
- $sName = $this->getCacheFilePrefix('js') . 'bx_analytics_tracker_' . bx_site_hash($sSource . $iRevision) . '.js';
- $sPath = BX_DIRECTORY_PATH_CACHE_PUBLIC . $sName;
- if(!file_exists($sPath)) {
- $sCode = @file_get_contents(BX_DIRECTORY_PATH_MODULES . $sSource);
- $sMinified = $sCode !== false ? BxDolMinify::getInstance()->minifyJs($sCode) : '';
- // written aside and moved in, so a page never loads a half-written file
- $sTemp = $sPath . '.' . uniqid() . '.tmp';
- if($sMinified === '' || @file_put_contents($sTemp, $sMinified) === false || !@rename($sTemp, $sPath)) {
- @unlink($sTemp);
- return $sUrl;
- }
- @chmod($sPath, BX_DOL_FILE_RIGHTS);
- }
- return BX_DOL_URL_CACHE_PUBLIC . $sName;
- }
- /**
- * A report block on a page: the dashboard script, once per page, and the report.
- *
- * @param string $sReport report name, see BxAnalyticsModule::getReportData
- * @param array $aData report data
- * @param object $oFilter the filter the data was built with
- * @param array $aParams block parameters (group, table...) the script sends back on reload
- */
- public function getBlock($sReport, $aData, $oFilter, $aParams = [])
- {
- $this->_addDashboardAssets($sReport);
- return $this->_getJsInit() . $this->getReportHtml($sReport, $aData, $oFilter, $aParams);
- }
- /**
- * Block HTML for a report, on first render and when the dashboard script reloads it.
- */
- public function getReportHtml($sReport, $aData, $oFilter, $aParams = [])
- {
- $sMethod = '_get' . bx_gen_method_name($sReport) . 'Html';
- if(!method_exists($this, $sMethod))
- return '';
- $sContent = $this->$sMethod($aData, $oFilter, $aParams);
- if($sContent === '')
- return '';
- return $this->parseHtmlByName('block.html', [
- 'report' => $sReport,
- 'params' => $this->_esc(json_encode((object)$aParams)),
- 'class' => str_replace('_', '-', $sReport),
- 'content' => $sContent,
- ]);
- }
- /**
- * A Studio section: blocks in a grid, the wide ones across all columns.
- *
- * @param array $aBlocks list of ['title' => heading or '', 'content' => HTML, 'wide' => bool]
- */
- public function getStudioSection($aBlocks)
- {
- $aCards = [];
- foreach($aBlocks as $aBlock) {
- if($aBlock['content'] === '' || $aBlock['content'] === [])
- continue;
- $aCards[] = [
- 'class' => !empty($aBlock['wide']) ? 'bx-analytics-studio-wide' : '',
- 'bx_if:title' => [
- 'condition' => !empty($aBlock['title']),
- 'content' => ['title' => !empty($aBlock['title']) ? $aBlock['title'] : ''],
- ],
- 'content' => $aBlock['content'],
- ];
- }
- return $this->parseHtmlByName('studio_section.html', [
- 'bx_repeat:cards' => $aCards,
- ]);
- }
- /**
- * Traffic block of the Studio dashboard.
- */
- public function getStudioDashboardTraffic($aOverview, $aRealtime, $aPlatforms)
- {
- $this->addStudioCss(['dashboard.css']);
- $aValues = array_map(function($a) { return $a['future'] ? null : $a['visitors']; }, $aOverview['series']);
- $iMax = max(array_merge([1], array_map('intval', $aValues)));
- $iCount = count($aValues);
- $aPoints = [];
- foreach($aValues as $i => $mixedValue)
- if($mixedValue !== null)
- $aPoints[] = round($iCount > 1 ? 100 * $i / ($iCount - 1) : 50, 2) . ',' . round(38 - 36 * $mixedValue / $iMax, 2);
- $aKpis = [];
- foreach(['visitors', 'pageviews'] as $sMetric) {
- $aKpi = $aOverview['kpis'][$sMetric];
- $aKpis[] = [
- 'title' => _t('_bx_analytics_metric_' . $sMetric),
- 'value' => $this->formatValue($sMetric, $aKpi['value']),
- 'change' => $this->_formatChange($aKpi['change']),
- 'trend' => $this->_trend($sMetric, $aKpi['change']),
- ];
- }
- $aRows = [];
- foreach($aPlatforms['items'] as $aItem)
- $aRows[] = [
- 'label' => $this->_esc($aItem['label']),
- 'share' => $aItem['share'],
- 'bar' => $aItem['bar'],
- ];
- return $this->parseHtmlByName('studio_traffic.html', [
- 'online' => $this->formatValue('number', $aRealtime['online']),
- 'online_title' => _t('_bx_analytics_txt_online_now', $aRealtime['window']),
- 'bx_repeat:kpis' => $aKpis,
- 'points' => implode(' ', $aPoints),
- 'chart_label' => _t('_bx_analytics_txt_chart_visitors_7d'),
- 'bx_if:platforms' => [
- 'condition' => !empty($aRows),
- 'content' => ['bx_repeat:platforms' => $aRows],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aRows),
- 'content' => ['empty' => _t('_bx_analytics_txt_no_data_yet')],
- ],
- 'link' => BX_DOL_URL_STUDIO . 'module.php?name=' . $this->MODULE . '&page=overview',
- 'link_title' => _t('_bx_analytics_txt_open_analytics'),
- ]);
- }
- /**
- * A number for display by its kind: number (950, 9.5K), percent, duration (seconds),
- * ms, pages_per_session, or a metric name.
- */
- public function formatValue($sType, $mixedValue)
- {
- if($mixedValue === null || $mixedValue === '')
- return '';
- switch($sType) {
- case 'bounce_rate':
- case 'percent':
- case 'rate':
- return (abs($mixedValue) < 10 && round($mixedValue) != $mixedValue ? number_format((float)$mixedValue, 1) : round($mixedValue)) . '%';
- // shares close to 100%, such as crash-free visits: 99.95%, not 100%
- case 'percent_precise':
- return rtrim(rtrim(number_format((float)$mixedValue, 2), '0'), '.') . '%';
- case 'pages_per_session':
- case 'decimal':
- return number_format((float)$mixedValue, 1);
- // good, needs_improvement or poor, as a coloured label
- case 'rating':
- return $this->parseHtmlByName('rating.html', [
- 'rating' => str_replace('_', '-', $this->_esc($mixedValue)),
- 'title' => _t('_bx_analytics_rating_' . preg_replace('/[^a-z_]/', '', (string)$mixedValue)),
- ]);
- // a small line of values over time
- case 'spark':
- return is_array($mixedValue) ? $this->_getSparkHtml($mixedValue) : '';
- case 'duration':
- case 'engaged':
- $iSeconds = (int)round($mixedValue);
- if($iSeconds < 60)
- return $iSeconds . _t('_bx_analytics_txt_duration_s');
- if($iSeconds < 3600)
- return intdiv($iSeconds, 60) . _t('_bx_analytics_txt_duration_m') . ' ' . ($iSeconds % 60) . _t('_bx_analytics_txt_duration_s');
- return intdiv($iSeconds, 3600) . _t('_bx_analytics_txt_duration_h') . ' ' . intdiv($iSeconds % 3600, 60) . _t('_bx_analytics_txt_duration_m');
- case 'ms':
- return $mixedValue >= 1000 ? number_format($mixedValue / 1000, 2) . _t('_bx_analytics_txt_duration_s') : round($mixedValue) . _t('_bx_analytics_txt_ms');
- case 'cls':
- return number_format((float)$mixedValue, 3);
- case 'money':
- // the site's default currency, the way the payment module shows it
- return $this->_esc(_t_format_currency((float)$mixedValue));
- case 'credits':
- return rtrim(rtrim(number_format((float)$mixedValue, 2), '0'), '.');
- case 'datetime':
- return bx_time_js((int)$mixedValue, BX_FORMAT_DATE_TIME, true);
- case 'date':
- return bx_time_js((int)$mixedValue, BX_FORMAT_DATE, true);
- case 'text':
- return $this->_esc($mixedValue);
- }
- return $this->_formatNumber($mixedValue);
- }
- protected function _getFiltersHtml($aData, $oFilter)
- {
- $aFilter = $oFilter->get();
- $aRanges = [];
- foreach($this->_aRanges as $sRange)
- $aRanges[] = [
- 'value' => $sRange,
- 'title' => _t('_bx_analytics_range_' . $sRange),
- 'selected' => $aFilter['range'] == $sRange ? 'selected="selected"' : '',
- ];
- if($aFilter['range'] == 'custom')
- $aRanges[] = [
- 'value' => 'custom',
- 'title' => $this->_esc($aFilter['from_date'] . ' - ' . $aFilter['to_date']),
- 'selected' => 'selected="selected"',
- ];
- $aPlatforms = [['value' => '', 'title' => _t('_bx_analytics_txt_all_platforms'), 'selected' => '']];
- foreach($this->_oConfig->getPlatforms() as $sCode => $sTitle)
- $aPlatforms[] = [
- 'value' => $sCode,
- 'title' => _t($sTitle),
- 'selected' => $aFilter['platform'] == $sCode ? 'selected="selected"' : '',
- ];
- $aChips = [];
- foreach($aData['chips'] as $aChip)
- $aChips[] = [
- 'name' => $aChip['name'],
- 'title' => $aChip['title'],
- 'value' => $this->_esc($aChip['label']),
- 'remove' => $this->_esc(_t('_bx_analytics_txt_remove_filter', $aChip['title'])),
- ];
- return $this->parseHtmlByName('filters.html', [
- 'range_label' => _t('_bx_analytics_txt_range'),
- 'bx_repeat:ranges' => $aRanges,
- 'platform_label' => _t('_bx_analytics_txt_platform'),
- 'bx_repeat:platforms' => $aPlatforms,
- 'bx_if:chips' => [
- 'condition' => !empty($aChips),
- 'content' => ['bx_repeat:chips' => $aChips],
- ],
- 'online' => $this->formatValue('number', $aData['online']),
- 'online_title' => _t('_bx_analytics_txt_online_now', $aData['window']),
- ]);
- }
- protected function _getOverviewHtml($aOverview, $oFilter)
- {
- $sMetric = bx_get('metric');
- $aMetrics = $this->getModule()->getReports()->getMetrics();
- if(!in_array($sMetric, $aMetrics, true))
- $sMetric = 'visitors';
- $aKpis = [];
- foreach($aOverview['kpis'] as $sName => $aKpi)
- $aKpis[] = [
- 'metric' => $sName,
- 'pressed' => $sName == $sMetric ? 'true' : 'false',
- 'title' => _t('_bx_analytics_metric_' . $sName),
- 'value' => $this->formatValue($sName, $aKpi['value']),
- 'change' => $this->_formatChange($aKpi['change']),
- 'change_title' => $this->_esc(_t('_bx_analytics_txt_previous_period', $this->formatValue($sName, $aKpi['previous']))),
- 'trend' => $this->_trend($sName, $aKpi['change']),
- ];
- $aTitles = [];
- foreach($aMetrics as $sName)
- $aTitles[$sName] = _t('_bx_analytics_metric_' . $sName);
- return $this->parseHtmlByName('overview.html', [
- 'kpis_label' => _t('_bx_analytics_txt_key_figures'),
- 'bx_repeat:kpis' => $aKpis,
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_label')),
- 'chart_json' => $this->_json([
- 'metric' => $sMetric,
- 'bucket' => $aOverview['bucket'],
- 'tz' => $aOverview['tz'],
- 'series' => $aOverview['series'],
- 'titles' => $aTitles,
- ]),
- ]);
- }
- protected function _getTopHtml($aTop, $oFilter)
- {
- $aItems = [];
- foreach($aTop['items'] as $aItem)
- $aItems[] = [
- 'dimension' => $aTop['dimension'],
- 'value' => $this->_esc($aItem['value']),
- 'label' => $this->_esc($aItem['label']),
- 'visitors' => $this->formatValue('number', $aItem['visitors']),
- 'share' => $aItem['share'],
- 'bar' => $aItem['bar'],
- 'filter_title' => $this->_esc(_t('_bx_analytics_txt_filter_by', $aItem['label'])),
- ];
- return $this->parseHtmlByName('top.html', [
- 'tabs' => $this->_getTabsHtml($aTop['group'], $aTop['tabs']),
- 'group' => $aTop['group'],
- 'column' => $aTop['column'],
- 'visitors' => _t('_bx_analytics_metric_visitors'),
- 'bx_if:items' => [
- 'condition' => !empty($aItems),
- 'content' => ['bx_repeat:items' => $aItems],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aItems),
- 'content' => ['empty' => _t('_bx_analytics_txt_no_data')],
- ],
- ]);
- }
- protected function _getRealtimeHtml($aData, $oFilter)
- {
- $iMax = 1;
- foreach($aData['minutes'] as $aMinute)
- $iMax = max($iMax, $aMinute['pageviews']);
- $aMinutes = [];
- foreach($aData['minutes'] as $aMinute)
- $aMinutes[] = [
- 'height' => $aMinute['pageviews'] ? max(4, round(100 * $aMinute['pageviews'] / $iMax)) : 0,
- 'title' => $this->_esc(_t('_bx_analytics_txt_pageviews_at', $aMinute['pageviews'], gmdate('H:i', $aMinute['t']) . ' UTC')),
- ];
- $aPlatforms = [];
- foreach($aData['platforms'] as $aPlatform)
- $aPlatforms[] = [
- 'label' => $this->_esc($aPlatform['label']),
- 'visitors' => $this->formatValue('number', $aPlatform['visitors']),
- 'share' => $aPlatform['share'],
- ];
- $aRecent = [];
- foreach($aData['recent'] as $aItem) {
- $aMeta = array_filter([
- $aItem['platform_label'],
- $aItem['city'] !== '' ? $aItem['city'] . ', ' . $aItem['country_label'] : ($aItem['country'] !== '' ? $aItem['country_label'] : ''),
- $aItem['device'],
- $aItem['member'] ? _t('_bx_analytics_txt_member') : '',
- ]);
- $aRecent[] = [
- 'time' => bx_time_js($aItem['ts'], BX_FORMAT_DATE_TIME),
- 'title' => $this->_esc($aItem['title'] !== '' ? $aItem['title'] : $aItem['path']),
- 'path' => $this->_esc($aItem['path']),
- 'meta' => $this->_esc(implode(' · ', $aMeta)),
- ];
- }
- return $this->parseHtmlByName('realtime.html', [
- 'online' => $this->formatValue('number', $aData['online']),
- 'online_title' => _t('_bx_analytics_txt_online_now', $aData['window']),
- 'bx_if:platforms' => [
- 'condition' => !empty($aPlatforms),
- 'content' => ['bx_repeat:platforms' => $aPlatforms],
- ],
- 'minutes_label' => $this->_esc(_t('_bx_analytics_txt_pageviews_30m')),
- 'bx_repeat:minutes' => $aMinutes,
- 'axis_start' => _t('_bx_analytics_txt_minutes_ago', 30),
- 'axis_end' => _t('_bx_analytics_txt_now'),
- 'recent_title' => _t('_bx_analytics_txt_latest_pageviews'),
- 'bx_if:recent' => [
- 'condition' => !empty($aRecent),
- 'content' => ['bx_repeat:recent' => $aRecent],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aRecent),
- 'content' => ['empty' => _t('_bx_analytics_txt_no_recent')],
- ],
- ]);
- }
- protected function _getMapHtml($aData, $oFilter)
- {
- $aCountries = [];
- foreach(array_slice($aData['countries'], 0, 12) as $aCountry)
- $aCountries[] = [
- 'value' => $this->_esc($aCountry['country']),
- 'label' => $this->_esc($aCountry['label']),
- 'visitors' => $this->formatValue('number', $aCountry['visitors']),
- 'share' => $aCountry['share'],
- 'bar' => $aCountry['bar'],
- 'filter_title' => $this->_esc(_t('_bx_analytics_txt_filter_by', $aCountry['label'])),
- ];
- return $this->parseHtmlByName('map.html', [
- 'tabs' => $this->_getTabsHtml('map', $aData['tabs']),
- 'refresh' => $aData['tab'] == 'live' ? 'data-bx-analytics-refresh="30"' : '',
- 'map_label' => $this->_esc(_t('_bx_analytics_txt_map_label')),
- 'map_json' => $this->_json([
- 'countries' => $aData['countries'],
- 'points' => $aData['points'],
- 'provider' => $aData['provider'],
- 'visitors' => _t('_bx_analytics_metric_visitors'),
- ]),
- 'column' => _t('_bx_analytics_tab_countries'),
- 'visitors' => _t('_bx_analytics_metric_visitors'),
- 'bx_if:countries' => [
- 'condition' => !empty($aCountries),
- 'content' => ['bx_repeat:countries' => $aCountries],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aCountries),
- 'content' => ['empty' => _t($aData['tab'] == 'live' ? '_bx_analytics_txt_no_recent' : '_bx_analytics_txt_no_data')],
- ],
- 'bx_if:unknown' => [
- 'condition' => $aData['unknown'] > 0,
- 'content' => ['unknown' => _t('_bx_analytics_txt_map_unknown', $this->formatValue('number', $aData['unknown']))],
- ],
- ]);
- }
- protected function _getBotsHtml($aData, $oFilter)
- {
- return $this->parseHtmlByName('bots.html', [
- 'tabs' => $this->_getTabsHtml('bots', $aData['tabs']),
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_bots')),
- 'chart_json' => $this->_json($aData['chart']),
- 'bots' => $this->_getTableHtml($aData['bots'], $oFilter),
- 'pages_title' => $aData['pages']['caption'],
- 'pages' => $this->_getTableHtml($aData['pages'], $oFilter),
- ]);
- }
- protected function _getEventsHtml($aData, $oFilter)
- {
- if($aData['view'] == 'list')
- return $this->_getTableHtml($aData, $oFilter);
- $aProps = [];
- foreach($aData['props'] as $aTable)
- $aProps[] = [
- 'title' => $this->_esc($aTable['caption']),
- 'table' => $this->_getTableHtml($aTable, $oFilter),
- ];
- return $this->parseHtmlByName('event.html', [
- 'back' => _t('_bx_analytics_txt_back_events'),
- 'title' => $this->_esc($aData['title']),
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_event', $aData['title'])),
- 'chart_json' => $this->_json($aData['chart']),
- 'platforms_title' => _t('_bx_analytics_tab_platforms'),
- 'platforms' => $this->_getBarsHtml($aData['platforms'], 'count'),
- 'bx_repeat:props' => $aProps,
- 'bx_if:no_props' => [
- 'condition' => empty($aProps),
- 'content' => [
- 'props_title' => _t('_bx_analytics_txt_properties'),
- 'no_props' => _t('_bx_analytics_txt_no_properties'),
- ],
- ],
- ]);
- }
- protected function _getSessionsHtml($aData, $oFilter)
- {
- if($aData['view'] == 'list')
- return $this->_getTableHtml($aData, $oFilter);
- return $this->_getSessionHtml($aData);
- }
- protected function _getUsersHtml($aData, $oFilter)
- {
- if($aData['view'] == 'list')
- return $this->_getTableHtml($aData, $oFilter);
- if($aData['view'] == 'detail')
- return $this->_getSessionHtml($aData);
- return $this->parseHtmlByName('user.html', [
- 'back' => _t('_bx_analytics_txt_back_members'),
- 'member' => $this->_getMemberHtml($aData['member']),
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'sessions_title' => _t('_bx_analytics_lmi_cpt_sessions'),
- 'sessions' => $this->_getTableHtml($aData['sessions'], $oFilter),
- 'platforms_title' => _t('_bx_analytics_tab_platforms'),
- 'platforms' => $this->_getBarsHtml($aData['platforms'], 'count'),
- 'actions_title' => $aData['actions']['caption'],
- 'actions' => $this->_getTableHtml($aData['actions'], $oFilter),
- ]);
- }
- protected function _getSessionHtml($aData)
- {
- $aFacts = [];
- foreach($aData['facts'] as $aFact)
- $aFacts[] = [
- 'title' => $aFact['title'],
- 'value' => $aFact['value'] === '' ? '—' : $this->formatValue($aFact['type'], $aFact['value']),
- ];
- $aKinds = [
- BX_ANALYTICS_HIT_PAGEVIEW => _t('_bx_analytics_event_type_pageview'),
- BX_ANALYTICS_HIT_EVENT => _t('_bx_analytics_event_type_event'),
- BX_ANALYTICS_HIT_UNA => _t('_bx_analytics_event_type_una'),
- ];
- $aTimeline = [];
- foreach($aData['timeline'] as $aItem)
- $aTimeline[] = [
- 'type' => $aItem['type'],
- 'offset' => sprintf('%d:%02d', intdiv($aItem['offset'], 60), $aItem['offset'] % 60),
- 'kind' => isset($aKinds[$aItem['type']]) ? $aKinds[$aItem['type']] : $this->_esc($aItem['type']),
- 'label' => $this->_esc($aItem['label']),
- 'bx_if:path' => [
- 'condition' => $aItem['path'] !== '' && $aItem['path'] !== $aItem['label'],
- 'content' => ['path' => $this->_esc($aItem['path'])],
- ],
- 'engaged' => $aItem['engaged'] ? $this->formatValue('duration', $aItem['engaged']) : '',
- ];
- return $this->parseHtmlByName('session.html', [
- 'back' => _t('_bx_analytics_txt_back_sessions'),
- 'member' => $this->_getMemberHtml($aData['member']),
- 'bx_repeat:facts' => $aFacts,
- 'timeline_title' => _t('_bx_analytics_txt_timeline'),
- 'bx_if:timeline' => [
- 'condition' => !empty($aTimeline),
- 'content' => ['bx_repeat:timeline' => $aTimeline],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aTimeline),
- 'content' => ['empty' => _t('_bx_analytics_txt_no_data')],
- ],
- ]);
- }
- protected function _getMemberHtml($aMember)
- {
- $bMember = !empty($aMember);
- return $this->parseHtmlByName('member.html', [
- 'bx_if:thumb' => [
- 'condition' => $bMember && !empty($aMember['thumb']),
- 'content' => ['thumb' => $bMember ? $this->_esc($aMember['thumb']) : ''],
- ],
- 'bx_if:no_thumb' => [
- 'condition' => !$bMember || empty($aMember['thumb']),
- 'content' => [],
- ],
- 'bx_if:link' => [
- 'condition' => $bMember,
- 'content' => [
- 'url' => $bMember ? $this->_esc($aMember['url']) : '',
- 'name' => $bMember ? $this->_esc($aMember['name']) : '',
- ],
- ],
- 'bx_if:text' => [
- 'condition' => !$bMember,
- 'content' => ['name' => _t('_bx_analytics_txt_guest')],
- ],
- ]);
- }
- /**
- * Shares as bars: items with label, a count under `$sKey` and share in percent.
- */
- protected function _getBarsHtml($aItems, $sKey)
- {
- $iMax = 1;
- foreach($aItems as $aItem)
- $iMax = max($iMax, (int)$aItem[$sKey]);
- $aRows = [];
- foreach($aItems as $aItem)
- $aRows[] = [
- 'label' => $this->_esc($aItem['label']),
- 'value' => $this->formatValue('number', $aItem[$sKey]),
- 'share' => $aItem['share'],
- 'bar' => round(100 * $aItem[$sKey] / $iMax, 1),
- ];
- return $this->parseHtmlByName('bars.html', [
- 'bx_if:items' => [
- 'condition' => !empty($aRows),
- 'content' => ['bx_repeat:items' => $aRows],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aRows),
- 'content' => ['empty' => _t('_bx_analytics_txt_no_data')],
- ],
- ]);
- }
- protected function _getJourneysHtml($aData, $oFilter)
- {
- $aEntries = [];
- foreach($aData['entries'] as $aEntry)
- $aEntries[] = [
- 'page' => $this->_esc($aEntry['page']),
- 'label' => $this->_esc($aEntry['page']),
- 'sessions' => $this->formatValue('number', $aEntry['sessions']),
- 'active' => $aEntry['active'] ? 'bx-analytics-row-btn-active' : '',
- 'pressed' => $aEntry['active'] ? 'true' : 'false',
- ];
- $fNodes = function($aItems, $bOpen) use ($aData) {
- $aResult = [];
- foreach($aItems as $aItem) {
- $bExit = !empty($aItem['exit']);
- $bActive = !$bExit && $bOpen && $aItem['page'] === $aData['via'];
- $aResult[] = [
- 'label' => $bExit ? _t('_bx_analytics_txt_left_site') : $this->_esc($aItem['page']),
- 'share' => $aItem['share'],
- 'class' => ($bExit ? 'bx-analytics-flow-exit' : '') . ($bActive ? ' bx-analytics-flow-node-active' : ''),
- 'attrs' => $bOpen && !$bExit ? 'data-bx-analytics-open="via" data-bx-analytics-value="' . $this->_esc($aItem['page']) . '" aria-pressed="' . ($bActive ? 'true' : 'false') . '"' : 'disabled',
- ];
- }
- return $aResult;
- };
- $iMax = 1;
- foreach($aData['paths'] as $aPath)
- $iMax = max($iMax, $aPath['sessions']);
- $aPathRows = [];
- foreach($aData['paths'] as $aPath)
- $aPathRows[] = [
- 'label' => implode(' → ', $aPath['pages']),
- 'bar' => round(100 * $aPath['sessions'] / $iMax, 1),
- 'values' => ['sessions' => $aPath['sessions'], 'share' => $aPath['share']],
- ];
- return $this->parseHtmlByName('journeys.html', [
- 'entries_title' => _t('_bx_analytics_txt_start_page'),
- 'bx_if:entries' => [
- 'condition' => !empty($aEntries),
- 'content' => ['bx_repeat:entries' => $aEntries],
- ],
- 'bx_if:no_entries' => [
- 'condition' => empty($aEntries),
- 'content' => ['empty' => _t('_bx_analytics_txt_no_data')],
- ],
- 'bx_if:flow' => [
- 'condition' => $aData['jfrom'] !== '',
- 'content' => [
- 'flow_label' => $this->_esc(_t('_bx_analytics_txt_journey_from', $aData['jfrom'])),
- 'step1' => _t('_bx_analytics_txt_step', 1),
- 'step2' => _t('_bx_analytics_txt_step', 2),
- 'step3' => _t('_bx_analytics_txt_step', 3),
- 'from' => $this->_esc($aData['jfrom']),
- 'from_sessions' => $this->formatValue('number', $aData['from_sessions']),
- 'bx_repeat:next' => $fNodes($aData['next'], true),
- 'bx_repeat:after' => $fNodes($aData['after'], false),
- 'bx_if:no_after' => [
- 'condition' => empty($aData['after']),
- 'content' => ['no_after' => _t('_bx_analytics_txt_pick_next')],
- ],
- ],
- ],
- 'paths_title' => _t('_bx_analytics_txt_top_paths'),
- 'paths' => $this->_getTableHtml([
- 'name' => 'paths',
- 'caption' => _t('_bx_analytics_txt_top_paths'),
- 'columns' => [
- ['key' => 'label', 'type' => 'label', 'title' => _t('_bx_analytics_txt_path')],
- ['key' => 'sessions', 'type' => 'number', 'title' => _t('_bx_analytics_metric_sessions')],
- ['key' => 'share', 'type' => 'percent', 'title' => _t('_bx_analytics_txt_share')],
- ],
- 'rows' => $aPathRows,
- ], $oFilter),
- 'bx_if:truncated' => [
- 'condition' => $aData['truncated'],
- 'content' => ['truncated' => _t('_bx_analytics_txt_journeys_truncated')],
- ],
- ]);
- }
- protected function _getRetentionHtml($aData, $oFilter)
- {
- $aWeeks = [];
- for($i = 0; $i < $aData['weeks']; $i++)
- $aWeeks[] = ['week' => _t('_bx_analytics_txt_week_n', $i)];
- $oTz = new DateTimeZone($aData['tz']);
- $aRows = [];
- foreach($aData['rows'] as $aRow) {
- $aCells = [];
- foreach($aRow['weeks'] as $mixedPercent)
- $aCells[] = [
- 'heat' => $mixedPercent !== null ? round($mixedPercent / 100, 3) : 0,
- 'strong' => $mixedPercent !== null && $mixedPercent >= 45 ? 'bx-analytics-cohort-strong' : '',
- 'percent' => $mixedPercent !== null && $aRow['size'] ? $this->formatValue('percent', $mixedPercent) : '',
- ];
- $aRows[] = [
- 'start' => (new DateTime('@' . $aRow['start']))->setTimezone($oTz)->format('Y-m-d'),
- 'size' => $this->formatValue('number', $aRow['size']),
- 'bx_repeat:cells' => $aCells,
- ];
- }
- return $this->parseHtmlByName('retention.html', [
- 'tabs' => $this->_getTabsHtml('retention', $aData['tabs']),
- 'bx_if:cookieless' => [
- 'condition' => $aData['cookieless'],
- 'content' => ['cookieless' => _t('_bx_analytics_txt_retention_cookieless')],
- ],
- 'caption' => $this->_esc(_t('_bx_analytics_lmi_cpt_retention')),
- 'cohort_title' => _t('_bx_analytics_txt_cohort'),
- 'size_title' => _t('_bx_analytics_metric_visitors'),
- 'bx_repeat:weeks' => $aWeeks,
- 'bx_repeat:rows' => $aRows,
- ]);
- }
- protected function _getGoalsHtml($aData, $oFilter)
- {
- $sCsrf = $aData['manage'] ? (string)BxDolForm::getCsrfToken() : '';
- $aTypes = ['page' => _t('_bx_analytics_txt_goal_type_page'), 'event' => _t('_bx_analytics_txt_goal_type_event')];
- $fMax = 0;
- foreach($aData['goals'] as $aGoal)
- $fMax = max($fMax, $aGoal['rate']);
- $aGoals = [];
- $aEdit = null;
- foreach($aData['goals'] as $aGoal) {
- if($aData['edit'] !== '' && $aGoal['id'] === (int)$aData['edit'])
- $aEdit = $aGoal;
- $aGoals[] = [
- 'id' => $aGoal['id'],
- 'name' => $this->_esc($aGoal['name']),
- 'type' => _t('_bx_analytics_txt_goal_badge_' . $aGoal['type']),
- 'pattern' => $this->_esc($aGoal['pattern']),
- 'rate' => $this->formatValue('percent', $aGoal['rate']),
- 'change' => $this->_formatChange($aGoal['change']),
- 'trend' => $this->_trend('', $aGoal['change']),
- 'conversions' => _t('_bx_analytics_txt_conversions', $this->formatValue('number', $aGoal['conversions']), $this->formatValue('number', $aGoal['visitors'])),
- 'bar' => $fMax ? round(100 * $aGoal['rate'] / $fMax, 1) : 0,
- 'bx_if:actions' => [
- 'condition' => $aData['manage'],
- 'content' => [
- 'id' => $aGoal['id'],
- 'edit' => _t('_bx_analytics_txt_edit'),
- 'delete' => _t('_bx_analytics_txt_delete'),
- 'confirm' => $this->_esc(_t('_bx_analytics_txt_goal_delete_confirm', $aGoal['name'])),
- 'csrf' => $this->_esc($sCsrf),
- ],
- ],
- ];
- }
- $bForm = $aData['manage'] && ($aEdit || $aData['edit'] === 'new');
- return $this->parseHtmlByName('goals.html', [
- 'bx_if:goals' => [
- 'condition' => !empty($aGoals),
- 'content' => ['bx_repeat:goals' => $aGoals],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aGoals),
- 'content' => ['empty' => _t('_bx_analytics_txt_no_goals')],
- ],
- 'bx_if:add' => [
- 'condition' => $aData['manage'] && !$bForm,
- 'content' => ['add' => _t('_bx_analytics_txt_add_goal')],
- ],
- 'bx_if:form' => [
- 'condition' => $bForm,
- 'content' => [
- 'form_title' => _t($aEdit ? '_bx_analytics_txt_edit_goal' : '_bx_analytics_txt_add_goal'),
- 'form_id' => $aEdit ? $aEdit['id'] : 0,
- 'form_csrf' => $this->_esc($sCsrf),
- 'form_name' => $aEdit ? $this->_esc($aEdit['name']) : '',
- 'form_pattern' => $aEdit ? $this->_esc($aEdit['pattern']) : '',
- 'page_selected' => !$aEdit || $aEdit['type'] == 'page' ? 'selected' : '',
- 'event_selected' => $aEdit && $aEdit['type'] == 'event' ? 'selected' : '',
- 'label_name' => _t('_bx_analytics_txt_name'),
- 'label_type' => _t('_bx_analytics_txt_goal_type'),
- 'label_pattern' => _t('_bx_analytics_txt_goal_match'),
- 'type_page' => $aTypes['page'],
- 'type_event' => $aTypes['event'],
- 'hint' => _t('_bx_analytics_txt_match_hint'),
- 'save' => _t('_bx_analytics_txt_save'),
- 'cancel' => _t('_bx_analytics_txt_cancel'),
- ],
- ],
- ]);
- }
- protected function _getFunnelsHtml($aData, $oFilter)
- {
- $sCsrf = $aData['manage'] ? (string)BxDolForm::getCsrfToken() : '';
- $aTypes = ['page' => _t('_bx_analytics_txt_goal_type_page'), 'event' => _t('_bx_analytics_txt_goal_type_event')];
- $aList = [];
- foreach($aData['funnels'] as $aFunnel)
- $aList[] = [
- 'id' => $aFunnel['id'],
- 'name' => $this->_esc($aFunnel['name']),
- 'active' => $aFunnel['active'] ? 'bx-analytics-chip-active' : '',
- 'pressed' => $aFunnel['active'] ? 'true' : 'false',
- ];
- $aSteps = [];
- foreach($aData['steps'] as $i => $aStep)
- $aSteps[] = [
- 'number' => $i + 1,
- 'pattern' => $this->_esc($aStep['pattern']),
- 'type' => _t('_bx_analytics_txt_goal_badge_' . $aStep['type']),
- 'rate_bar' => $aStep['rate'],
- 'sessions' => _t('_bx_analytics_txt_n_sessions', $this->formatValue('number', $aStep['sessions'])),
- 'rate' => $this->formatValue('percent', $aStep['rate']),
- 'bx_if:drop' => [
- 'condition' => $aStep['drop'] !== null,
- 'content' => ['drop' => _t('_bx_analytics_txt_dropped', $this->formatValue('percent', (float)$aStep['drop']))],
- ],
- ];
- $sEdit = (string)$aData['edit'];
- $bNew = $sEdit === 'new';
- $bEdit = $aData['funnel'] && $sEdit !== '' && (int)$sEdit === $aData['funnel']['id'];
- $bForm = $aData['manage'] && ($bNew || $bEdit);
- $aRows = [];
- $aDefinition = $bEdit ? $aData['definition'] : [];
- for($i = 0; $i < $aData['max_steps']; $i++) {
- $aStep = isset($aDefinition[$i]) ? $aDefinition[$i] : ['type' => 'page', 'pattern' => ''];
- $aRows[] = [
- 'row_number' => $i + 1,
- 'row_type_label' => _t('_bx_analytics_txt_step_type', $i + 1),
- 'row_pattern_label' => _t('_bx_analytics_txt_step_match', $i + 1),
- 'row_page' => $aStep['type'] == 'page' ? 'selected' : '',
- 'row_type_page' => $aTypes['page'],
- 'row_type_event' => $aTypes['event'],
- 'row_event' => $aStep['type'] == 'event' ? 'selected' : '',
- 'row_pattern' => $this->_esc($aStep['pattern']),
- ];
- }
- return $this->parseHtmlByName('funnels.html', [
- 'bx_if:list' => [
- 'condition' => count($aList) > 1,
- 'content' => [
- 'list_label' => $this->_esc(_t('_bx_analytics_lmi_cpt_funnels')),
- 'bx_repeat:list' => $aList,
- ],
- ],
- 'bx_if:manage' => [
- 'condition' => $aData['manage'] && !$bForm,
- 'content' => [
- 'add' => _t('_bx_analytics_txt_add_funnel'),
- 'bx_if:current' => [
- 'condition' => (bool)$aData['funnel'],
- 'content' => [
- 'current_id' => $aData['funnel'] ? $aData['funnel']['id'] : 0,
- 'edit' => _t('_bx_analytics_txt_edit'),
- 'delete' => _t('_bx_analytics_txt_delete'),
- 'confirm' => $this->_esc(_t('_bx_analytics_txt_funnel_delete_confirm', $aData['funnel'] ? $aData['funnel']['name'] : '')),
- 'csrf' => $this->_esc($sCsrf),
- ],
- ],
- ],
- ],
- 'bx_if:steps' => [
- 'condition' => !empty($aSteps) && !$bForm,
- 'content' => ['bx_repeat:steps' => $aSteps],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aSteps) && !$bForm,
- 'content' => ['empty' => _t('_bx_analytics_txt_no_funnels')],
- ],
- 'bx_if:form' => [
- 'condition' => $bForm,
- 'content' => [
- 'form_title' => _t($bEdit ? '_bx_analytics_txt_edit_funnel' : '_bx_analytics_txt_add_funnel'),
- 'form_id' => $bEdit ? $aData['funnel']['id'] : 0,
- 'form_csrf' => $this->_esc($sCsrf),
- 'form_name' => $bEdit ? $this->_esc($aData['funnel']['name']) : '',
- 'label_name' => _t('_bx_analytics_txt_name'),
- 'bx_repeat:rows' => $aRows,
- 'hint' => _t('_bx_analytics_txt_funnel_hint'),
- 'save' => _t('_bx_analytics_txt_save'),
- 'cancel' => _t('_bx_analytics_txt_cancel'),
- ],
- ],
- ]);
- }
- protected function _getRevenueHtml($aData, $oFilter)
- {
- return $this->parseHtmlByName('revenue.html', [
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_revenue')),
- 'chart_json' => $this->_json($aData['chart']),
- 'split' => $this->_getTableHtml($aData['split'], $oFilter),
- 'notes' => $this->_getNotesHtml($aData['notes']),
- 'import' => $this->_getImportHtml($aData['import']),
- ]);
- }
- protected function _getSubscriptionsHtml($aData, $oFilter)
- {
- return $this->parseHtmlByName('subscriptions.html', [
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_mrr')),
- 'chart_json' => $this->_json($aData['chart']),
- 'movement_title' => _t('_bx_analytics_txt_sbs_movement'),
- 'movement_label' => $this->_esc(_t('_bx_analytics_txt_sbs_movement')),
- 'movement_json' => $this->_json($aData['movement']),
- 'plans_title' => $this->_esc($aData['plans']['caption']),
- 'plans' => $this->_getTableHtml($aData['plans'], $oFilter),
- 'reasons_title' => $this->_esc($aData['reasons']['caption']),
- 'reasons' => $this->_getTableHtml($aData['reasons'], $oFilter),
- 'notes' => $this->_getNotesHtml($aData['notes']),
- ]);
- }
- protected function _getSalesHtml($aData, $oFilter)
- {
- return $this->parseHtmlByName('sales.html', [
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'items_title' => $this->_esc($aData['items']['caption']),
- 'items' => $this->_getTableHtml($aData['items'], $oFilter),
- 'sellers_title' => $this->_esc($aData['sellers']['caption']),
- 'sellers' => $this->_getTableHtml($aData['sellers'], $oFilter),
- 'bx_if:invoices' => [
- 'condition' => !empty($aData['invoices']),
- 'content' => [
- 'invoices_title' => _t('_bx_analytics_txt_sales_invoices'),
- 'invoices' => !empty($aData['invoices']) ? $this->_getTilesHtml($aData['invoices']) : '',
- ],
- ],
- 'notes' => $this->_getNotesHtml($aData['notes']),
- ]);
- }
- protected function _getCreditsHtml($aData, $oFilter)
- {
- if(empty($aData['available']))
- return $this->parseHtmlByName('credits.html', [
- 'bx_if:credits' => ['condition' => false, 'content' => []],
- 'bx_if:none' => [
- 'condition' => true,
- 'content' => ['empty' => _t('_bx_analytics_txt_credits_none')],
- ],
- 'notes' => '',
- ]);
- return $this->parseHtmlByName('credits.html', [
- 'bx_if:credits' => [
- 'condition' => true,
- 'content' => [
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_credits')),
- 'chart_json' => $this->_json($aData['chart']),
- 'spenders_title' => $this->_esc($aData['spenders']['caption']),
- 'spenders' => $this->_getTableHtml($aData['spenders'], $oFilter),
- 'spent_title' => $this->_esc($aData['spent']['caption']),
- 'spent' => $this->_getTableHtml($aData['spent'], $oFilter),
- ],
- ],
- 'bx_if:none' => ['condition' => false, 'content' => []],
- 'notes' => $this->_getNotesHtml($aData['notes']),
- ]);
- }
- /**
- * Errors: the list with its key figures, or one error group. Messages, sources and
- * stacks come from visitors' browsers and apps: every one is escaped.
- */
- protected function _getErrorsHtml($aData, $oFilter)
- {
- if($aData['view'] == 'list')
- return $this->parseHtmlByName('errors.html', [
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'table' => $this->_getTableHtml($aData, $oFilter),
- ]);
- if(!empty($aData['missing']))
- return $this->parseHtmlByName('error_missing.html', [
- 'back' => _t('_bx_analytics_txt_back_errors'),
- 'text' => $this->_esc($aData['title']),
- ]);
- $sCsrf = $aData['manage'] ? (string)BxDolForm::getCsrfToken() : '';
- $aActions = [];
- foreach($aData['actions'] as $aAction)
- $aActions[] = [
- 'action_op' => $aAction['op'],
- 'action_id' => (int)$aData['id'],
- 'action_csrf' => $this->_esc($sCsrf),
- 'action_title' => $aAction['title'],
- ];
- $aFacts = [];
- foreach($aData['facts'] as $aFact)
- $aFacts[] = [
- 'fact_title' => $aFact['title'],
- 'fact_value' => $this->formatValue($aFact['type'], $aFact['value']),
- ];
- $aBreakdowns = [];
- foreach($aData['breakdowns'] as $aTable)
- $aBreakdowns[] = [
- 'breakdown_title' => $this->_esc($aTable['caption']),
- 'breakdown' => $this->_getTableHtml($aTable, $oFilter),
- ];
- $bStack = $aData['stack'] !== '';
- return $this->parseHtmlByName('error.html', [
- 'back' => _t('_bx_analytics_txt_back_errors'),
- 'title' => $this->_esc($aData['title']),
- 'bx_if:actions' => [
- 'condition' => !empty($aActions),
- 'content' => [
- 'actions_label' => $this->_esc(_t('_bx_analytics_txt_error_actions')),
- 'bx_repeat:actions' => $aActions,
- ],
- ],
- 'bx_repeat:facts' => $aFacts,
- 'tiles' => $this->_getTilesHtml($aData['tiles']),
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_error', $aData['title'])),
- 'chart_json' => $this->_json($aData['chart']),
- 'stack_title' => _t('_bx_analytics_txt_error_stack'),
- 'bx_if:no_stack' => [
- 'condition' => !$bStack,
- 'content' => ['no_stack' => _t($aData['stack_hidden'] ? '_bx_analytics_txt_error_stack_hidden' : '_bx_analytics_txt_error_no_stack')],
- ],
- 'bx_if:breakdowns' => [
- 'condition' => !empty($aBreakdowns),
- 'content' => ['bx_repeat:breakdowns' => $aBreakdowns],
- ],
- 'latest_title' => $this->_esc($aData['occurrences']['caption']),
- 'latest' => $this->_getTableHtml($aData['occurrences'], $oFilter),
- // last: visitor text goes in after every placeholder is filled
- 'bx_if:stack' => [
- 'condition' => $bStack,
- 'content' => [
- 'stack_label' => $this->_esc(_t('_bx_analytics_txt_error_stack')),
- 'stack' => $this->_esc($aData['stack']),
- ],
- ],
- ]);
- }
- /**
- * Performance: a card per metric (75th percentile, rating, distribution) that picks the
- * metric for the chart and the tables.
- */
- protected function _getPerformanceHtml($aData, $oFilter)
- {
- $aDevices = [];
- foreach($aData['devices'] as $aDevice)
- $aDevices[] = [
- 'device_value' => $aDevice['value'],
- 'device_title' => $aDevice['title'],
- 'device_selected' => $aDevice['selected'] ? 'selected="selected"' : '',
- ];
- $aCards = [];
- foreach($aData['cards'] as $aCard)
- $aCards[] = [
- 'card_metric' => $aCard['metric'],
- 'card_title' => $aCard['title'],
- 'card_value' => $aCard['value'] !== null ? $this->formatValue($aCard['unit'], $aCard['value']) : _t('_bx_analytics_txt_not_enough_data'),
- 'card_rating' => $aCard['rating'] !== '' ? str_replace('_', '-', $aCard['rating']) : 'none',
- 'card_rating_title' => $aCard['rating'] !== '' ? _t('_bx_analytics_rating_' . $aCard['rating']) : '',
- 'card_pressed' => $aCard['active'] ? 'true' : 'false',
- 'card_good' => $aCard['good'],
- 'card_ni' => $aCard['needs_improvement'],
- 'card_poor' => $aCard['poor'],
- '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'])) : '',
- 'card_samples' => _t('_bx_analytics_txt_n_samples', $this->formatValue('number', $aCard['samples'])),
- 'card_thresholds' => _t('_bx_analytics_txt_vital_thresholds', $this->formatValue($aCard['unit'], $aCard['good_max']), $this->formatValue($aCard['unit'], $aCard['poor_min'])),
- ];
- return $this->parseHtmlByName('performance.html', [
- 'device_label' => _t('_bx_analytics_txt_device'),
- 'bx_repeat:devices' => $aDevices,
- 'cards_label' => $this->_esc(_t('_bx_analytics_txt_vitals_metrics')),
- 'bx_repeat:cards' => $aCards,
- 'chart_title' => $this->_esc(_t('_bx_analytics_txt_chart_vital', $aData['metric_title'])),
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_vital', $aData['metric_title'])),
- 'chart_json' => $this->_json($aData['chart']),
- 'platforms_title' => $this->_esc($aData['platforms']['caption']),
- 'platforms' => $this->_getTableHtml($aData['platforms'], $oFilter),
- 'pages_title' => $this->_esc($aData['pages']['caption']),
- 'pages' => $this->_getTableHtml($aData['pages'], $oFilter),
- ]);
- }
- /**
- * A sparkline of values over time, scaled to its own maximum.
- */
- protected function _getSparkHtml($aValues)
- {
- $aValues = array_values($aValues);
- $iCount = count($aValues);
- if(!$iCount)
- return '';
- $fMax = max(1, max($aValues));
- $aPoints = [];
- foreach($aValues as $i => $mixedValue)
- $aPoints[] = round($iCount > 1 ? 100 * $i / ($iCount - 1) : 50, 2) . ',' . round(22 - 20 * (float)$mixedValue / $fMax, 2);
- if($iCount == 1)
- $aPoints[] = '100,' . round(22 - 20 * (float)$aValues[0] / $fMax, 2);
- return $this->parseHtmlByName('spark.html', [
- 'points' => implode(' ', $aPoints),
- ]);
- }
- /**
- * Short notes under a report: how amounts are counted, what is missing.
- */
- protected function _getNotesHtml($aNotes)
- {
- if(empty($aNotes))
- return '';
- $aItems = [];
- foreach($aNotes as $sNote)
- $aItems[] = ['note' => $this->_esc($sNote)];
- return $this->parseHtmlByName('notes.html', [
- 'bx_repeat:notes' => $aItems,
- ]);
- }
- /**
- * The import of past payments, with a button to run a step now, for administrators.
- */
- protected function _getImportHtml($aImport)
- {
- if(empty($aImport) || empty($aImport['state']))
- return '';
- $bRunning = $aImport['state'] == 'running';
- return $this->parseHtmlByName('import.html', [
- 'title' => _t('_bx_analytics_txt_import_title'),
- 'text' => $this->_esc(_t($bRunning ? '_bx_analytics_txt_import_running' : '_bx_analytics_txt_import_done', $this->formatValue('number', $aImport['imported']))),
- 'hint' => _t('_bx_analytics_txt_import_hint'),
- 'button' => _t($bRunning ? '_bx_analytics_txt_import_continue' : '_bx_analytics_txt_import_again'),
- 'op' => $bRunning ? 'import' : 'restart',
- 'confirm' => $bRunning ? '' : $this->_esc(_t('_bx_analytics_txt_import_confirm')),
- 'csrf' => $this->_esc((string)BxDolForm::getCsrfToken()),
- ]);
- }
- /**
- * Key figures that are not chart switches: title, value and an optional note or change.
- */
- protected function _getTilesHtml($aTiles)
- {
- $aItems = [];
- foreach($aTiles as $aTile)
- $aItems[] = [
- 'title' => $aTile['title'],
- // nothing to measure (a rate without a base): say so instead of an empty tile
- 'value' => $aTile['value'] === null ? _t('_bx_analytics_txt_not_available') : $this->formatValue(isset($aTile['type']) ? $aTile['type'] : 'number', $aTile['value']),
- 'bx_if:sub' => [
- 'condition' => !empty($aTile['sub']),
- 'content' => ['sub' => !empty($aTile['sub']) ? $this->_esc($aTile['sub']) : ''],
- ],
- 'bx_if:change' => [
- 'condition' => isset($aTile['change']) && $aTile['change'] !== null,
- 'content' => [
- 'change' => isset($aTile['change']) ? $this->_formatChange($aTile['change']) : '',
- 'trend' => isset($aTile['change']) ? $this->_trend(isset($aTile['metric']) ? $aTile['metric'] : '', $aTile['change']) : 'flat',
- ],
- ],
- ];
- return $this->parseHtmlByName('tiles.html', [
- 'bx_repeat:tiles' => $aItems,
- ]);
- }
- /**
- * Query: what to break down and sort by, the first values over time, the table, and the
- * CSV download of every value.
- */
- protected function _getQueryHtml($aData, $oFilter)
- {
- $aDimensions = ['event' => [], 'session' => []];
- foreach($aData['dimensions'] as $aDimension)
- $aDimensions[$aDimension['kind']][] = [
- 'dim_value' => $aDimension['name'],
- 'dim_title' => $aDimension['title'],
- 'dim_selected' => $aDimension['name'] == $aData['dimension'] ? 'selected="selected"' : '',
- ];
- $aMetrics = [];
- foreach($aData['metrics'] as $aMetric)
- $aMetrics[] = [
- 'metric_value' => $aMetric['name'],
- 'metric_title' => $aMetric['title'],
- 'metric_selected' => $aMetric['name'] == $aData['metric'] ? 'selected="selected"' : '',
- ];
- $aDirs = [];
- foreach(['desc', 'asc'] as $sDir)
- $aDirs[] = [
- 'dir_value' => $sDir,
- 'dir_title' => _t('_bx_analytics_txt_sort_' . $sDir),
- 'dir_selected' => $sDir == $aData['dir'] ? 'selected="selected"' : '',
- ];
- $bRows = !empty($aData['rows']);
- $sExportUrl = bx_append_url_params(BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri() . 'export/query/', http_build_query(array_merge($oFilter->getParams(), [
- 'q_dim' => $aData['dimension'],
- 'q_metric' => $aData['metric'],
- 'q_dir' => $aData['dir'],
- ]), '', '&', PHP_QUERY_RFC3986));
- return $this->parseHtmlByName('query.html', [
- 'dimension_label' => _t('_bx_analytics_txt_dimension'),
- 'event_dims_label' => _t('_bx_analytics_txt_dims_event'),
- 'bx_repeat:event_dims' => $aDimensions['event'],
- 'session_dims_label' => _t('_bx_analytics_txt_dims_session'),
- 'bx_repeat:session_dims' => $aDimensions['session'],
- 'metric_label' => _t('_bx_analytics_txt_metric'),
- 'bx_repeat:metrics' => $aMetrics,
- 'dir_label' => _t('_bx_analytics_txt_sort'),
- 'bx_repeat:dirs' => $aDirs,
- 'bx_if:download' => [
- 'condition' => $bRows,
- 'content' => [
- 'export_url' => $this->_esc($sExportUrl),
- 'download' => _t('_bx_analytics_txt_download_csv'),
- ],
- ],
- 'bx_if:chart' => [
- 'condition' => $bRows,
- 'content' => [
- 'chart_label' => $this->_esc(_t('_bx_analytics_txt_chart_query', _t('_bx_analytics_metric_' . $aData['metric']), count($aData['chart']['series']))),
- 'chart_json' => $this->_json($aData['chart']),
- ],
- ],
- 'table' => $this->_getTableHtml($aData, $oFilter),
- 'bx_if:truncated' => [
- 'condition' => $aData['total'] > $aData['export_max'],
- 'content' => [
- 'truncated' => _t('_bx_analytics_txt_csv_truncated', $this->formatValue('number', $aData['export_max']), $this->formatValue('number', $aData['total'])),
- ],
- ],
- ]);
- }
- /**
- * A table report: columns with types, rows with a label cell that filters, links or is plain.
- */
- protected function _getTableHtml($aTable, $oFilter)
- {
- $aColumns = [];
- foreach($aTable['columns'] as $aColumn)
- $aColumns[] = [
- 'type' => $aColumn['type'],
- 'title' => $aColumn['title'],
- // the column the rows are sorted by: ascending or descending
- 'sort' => isset($aColumn['sort']) && in_array($aColumn['sort'], ['ascending', 'descending'], true) ? 'aria-sort="' . $aColumn['sort'] . '"' : '',
- ];
- $aRows = [];
- foreach($aTable['rows'] as $aRow) {
- $aCells = [];
- foreach($aTable['columns'] as $aColumn) {
- $sKey = $aColumn['key'];
- $bLabel = $aColumn['type'] == 'label';
- $aCells[] = [
- 'type' => $aColumn['type'],
- 'bx_if:label' => [
- 'condition' => $bLabel,
- 'content' => $bLabel ? $this->_getLabelCell($aRow) : [],
- ],
- 'bx_if:value' => [
- 'condition' => !$bLabel,
- 'content' => [
- 'cell_value' => !$bLabel ? $this->formatValue($aColumn['type'], isset($aRow['values'][$sKey]) ? $aRow['values'][$sKey] : null) : '',
- ],
- ],
- ];
- }
- $aRows[] = ['bx_repeat:cells' => $aCells];
- }
- $aPager = isset($aTable['pages']) && $aTable['pages'] > 1 ? [
- 'name' => $aTable['name'],
- 'page' => $aTable['page'],
- 'pages' => $aTable['pages'],
- 'prev' => max(1, $aTable['page'] - 1),
- 'next' => min($aTable['pages'], $aTable['page'] + 1),
- 'prev_disabled' => $aTable['page'] <= 1 ? 'disabled' : '',
- 'next_disabled' => $aTable['page'] >= $aTable['pages'] ? 'disabled' : '',
- 'txt_prev' => _t('_bx_analytics_txt_prev'),
- 'txt_next' => _t('_bx_analytics_txt_next'),
- 'txt_page' => _t('_bx_analytics_txt_page_of', $aTable['page'], $aTable['pages']),
- ] : [];
- return $this->parseHtmlByName('table.html', [
- 'tabs' => !empty($aTable['tabs']) ? $this->_getTabsHtml($aTable['name'], $aTable['tabs']) : '',
- 'panel_attrs' => !empty($aTable['tabs']) ? 'role="tabpanel" id="bx-analytics-panel-' . $aTable['name'] . '"' : '',
- 'name' => $aTable['name'],
- 'caption' => $this->_esc($aTable['caption']),
- 'bx_if:rows' => [
- 'condition' => !empty($aRows),
- 'content' => [
- 'caption' => $this->_esc($aTable['caption']),
- 'bx_repeat:columns' => $aColumns,
- 'bx_repeat:rows' => $aRows,
- ],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aRows),
- 'content' => ['empty' => !empty($aTable['empty']) ? $aTable['empty'] : _t('_bx_analytics_txt_no_data')],
- ],
- 'bx_if:pager' => [
- 'condition' => !empty($aPager),
- 'content' => $aPager,
- ],
- ]);
- }
- protected function _getLabelCell($aRow)
- {
- $sLabel = $this->_esc($aRow['label']);
- $sSub = isset($aRow['sub']) ? $this->_esc($aRow['sub']) : '';
- $bFilter = !empty($aRow['filter']);
- $bLink = !$bFilter && !empty($aRow['link']);
- $bOpen = !$bFilter && !$bLink && !empty($aRow['open']);
- return [
- 'bar' => isset($aRow['bar']) ? $aRow['bar'] : 0,
- // placeholder names are unique across the row: the parser fills every one with
- // the same name from any part of the row
- 'bx_if:filter' => [
- 'condition' => $bFilter,
- 'content' => [
- 'filter_dim' => $bFilter ? $aRow['filter']['dim'] : '',
- 'filter_value' => $bFilter ? $this->_esc($aRow['filter']['value']) : '',
- 'filter_title' => $this->_esc(_t('_bx_analytics_txt_filter_by', $aRow['label'])),
- 'label' => $sLabel,
- ],
- ],
- 'bx_if:link' => [
- 'condition' => $bLink,
- 'content' => [
- 'link_href' => $bLink ? $this->_esc($aRow['link']) : '',
- 'label' => $sLabel,
- ],
- ],
- 'bx_if:open' => [
- 'condition' => $bOpen,
- 'content' => [
- 'open_param' => $bOpen ? $aRow['open']['param'] : '',
- 'open_value' => $bOpen ? $this->_esc($aRow['open']['value']) : '',
- 'label' => $sLabel,
- ],
- ],
- 'bx_if:text' => [
- 'condition' => !$bFilter && !$bLink && !$bOpen,
- 'content' => ['label' => $sLabel],
- ],
- 'bx_if:sub' => [
- 'condition' => $sSub !== '',
- 'content' => ['sub' => $sSub],
- ],
- ];
- }
- protected function _getTabsHtml($sGroup, $aTabs)
- {
- if(count($aTabs) < 2)
- return '';
- $aItems = [];
- foreach($aTabs as $aTab)
- $aItems[] = [
- 'group' => $sGroup,
- 'name' => $aTab['name'],
- 'title' => $aTab['title'],
- 'selected' => $aTab['active'] ? 'true' : 'false',
- 'tabindex' => $aTab['active'] ? '0' : '-1',
- ];
- return $this->parseHtmlByName('tabs.html', [
- 'group' => $sGroup,
- 'label' => $this->_esc(_t('_bx_analytics_group_' . $sGroup)),
- 'bx_repeat:tabs' => $aItems,
- ]);
- }
- /**
- * Studio > API: pick a report, edit its parameters, see the call as bx_srv, as an
- * api.php URL and as NEO's request, and run it.
- *
- * @param array $aData catalog (see BxAnalyticsModule::getApiCatalog), api_enabled, api_access
- */
- public function getStudioApi($aData)
- {
- $aCatalog = $aData['catalog'];
- $aNotices = [];
- if(!$aData['api_enabled'])
- $aNotices[] = ['notice' => $this->_esc(_t('_bx_analytics_txt_api_disabled'))];
- elseif(!$aData['api_access'])
- $aNotices[] = ['notice' => $this->_esc(_t('_bx_analytics_txt_api_no_access'))];
- // a top list first: it shows what parameters do
- $sSelected = isset($aCatalog['reports']['top']) ? 'top' : (string)key($aCatalog['reports']);
- $aReports = [];
- $aDocs = [];
- foreach($aCatalog['reports'] as $sReport => $aParams) {
- $aReports[] = [
- 'report_value' => $sReport,
- 'report_title' => $this->_esc(_t('_bx_analytics_report_' . $sReport)),
- 'report_selected' => $sReport == $sSelected ? 'selected="selected"' : '',
- ];
- $aDocs[] = [
- 'doc_report' => $sReport,
- 'doc_hidden' => $sReport == $sSelected ? '' : 'hidden',
- 'doc_table' => $this->_getApiParamsHtml($aParams, _t('_bx_analytics_txt_api_report_params', _t('_bx_analytics_report_' . $sReport))),
- ];
- }
- $this->_addToolsAssets();
- return $this->_getToolsJsInit() . $this->parseHtmlByName('studio_api.html', [
- 'bx_if:notices' => [
- 'condition' => !empty($aNotices),
- 'content' => ['bx_repeat:notices' => $aNotices],
- ],
- 'intro' => _t('_bx_analytics_txt_api_intro'),
- 'csrf' => $this->_esc((string)BxDolForm::getCsrfToken()),
- 'report_label' => _t('_bx_analytics_txt_api_report'),
- 'bx_repeat:reports' => $aReports,
- 'params_label' => _t('_bx_analytics_txt_api_params'),
- 'params_hint' => _t('_bx_analytics_txt_api_params_hint'),
- 'docs_label' => _t('_bx_analytics_txt_api_docs'),
- 'params_default' => $this->_esc($sSelected == 'top' ? '{"group": "pages", "range": "7d"}' : '{"range": "7d"}'),
- 'common_table' => $this->_getApiParamsHtml($aCatalog['common'], _t('_bx_analytics_txt_api_common_params')),
- 'bx_repeat:docs' => $aDocs,
- 'code_srv' => $this->_getCodeHtml('bx-analytics-api-srv', _t('_bx_analytics_txt_api_srv'), ''),
- 'code_url' => $this->_getCodeHtml('bx-analytics-api-url', _t('_bx_analytics_txt_api_url'), ''),
- 'code_neo' => $this->_getCodeHtml('bx-analytics-api-neo', _t('_bx_analytics_txt_api_neo'), ''),
- 'run' => _t('_bx_analytics_txt_api_run'),
- 'response_label' => _t('_bx_analytics_txt_api_response'),
- 'copy' => _t('_bx_analytics_txt_copy'),
- 'copied' => $this->_esc(_t('_bx_analytics_txt_copied')),
- 'catalog_json' => $this->_json([
- 'root' => BX_DOL_URL_ROOT,
- 'module' => $this->MODULE,
- 'reports' => array_keys($aCatalog['reports']),
- ]),
- ]);
- }
- /**
- * A table of parameters: name and what it accepts.
- */
- protected function _getApiParamsHtml($aParams, $sCaption)
- {
- $aRows = [];
- foreach($aParams as $sName => $aParam) {
- switch($aParam['type']) {
- case 'enum':
- $sAccepts = _t('_bx_analytics_txt_api_type_enum', implode(', ', $aParam['values']));
- break;
- case 'enum_by':
- $aLists = [];
- foreach($aParam['values'] as $sBy => $aValues)
- $aLists[] = $sBy . ': ' . implode(', ', $aValues);
- $sAccepts = _t('_bx_analytics_txt_api_type_enum_by', $aParam['by'], implode('; ', $aLists));
- break;
- case 'int':
- $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']);
- break;
- case 'date':
- $sAccepts = _t('_bx_analytics_txt_api_type_date');
- break;
- case 'dims':
- $sAccepts = _t('_bx_analytics_txt_api_type_dims', implode(', ', $aParam['values']));
- break;
- default:
- $sAccepts = _t('_bx_analytics_txt_api_type_string');
- }
- if(isset($aParam['default']))
- $sAccepts .= ' ' . _t('_bx_analytics_txt_api_default', $aParam['default']);
- $aRows[] = [
- 'param_name' => $this->_esc($sName),
- 'param_accepts' => $this->_esc($sAccepts),
- ];
- }
- return $this->parseHtmlByName('api_params.html', [
- 'caption' => $this->_esc($sCaption),
- 'bx_if:params' => [
- 'condition' => !empty($aRows),
- 'content' => [
- 'caption' => $this->_esc($sCaption),
- 'col_param' => _t('_bx_analytics_txt_api_col_param'),
- 'col_accepts' => _t('_bx_analytics_txt_api_col_accepts'),
- 'bx_repeat:params' => $aRows,
- ],
- ],
- 'bx_if:none' => [
- 'condition' => empty($aRows),
- 'content' => ['none' => _t('_bx_analytics_txt_api_no_params')],
- ],
- ]);
- }
- /**
- * Studio > Dashboards: site pages with analytics blocks, and a form for a new one.
- *
- * @param array $aData pages (see BxAnalyticsModule::getDashboards)
- */
- public function getStudioDashboards($aData)
- {
- $aPages = [];
- foreach($aData['pages'] as $aPage)
- $aPages[] = [
- 'view_url' => $this->_esc($aPage['view_url']),
- 'page_title' => $this->_esc($aPage['title']),
- 'blocks' => $this->formatValue('number', $aPage['blocks']),
- 'api_blocks' => $this->formatValue('number', $aPage['api_blocks']),
- 'edit_url' => $this->_esc($aPage['edit_url']),
- 'edit' => _t('_bx_analytics_txt_dash_edit'),
- 'edit_label' => $this->_esc(_t('_bx_analytics_txt_dash_edit_label', $aPage['title'])),
- ];
- $this->_addToolsAssets();
- return $this->_getToolsJsInit() . $this->parseHtmlByName('studio_dashboards.html', [
- 'intro' => _t('_bx_analytics_txt_dash_intro'),
- 'bx_if:pages' => [
- 'condition' => !empty($aPages),
- 'content' => [
- 'caption' => $this->_esc(_t('_bx_analytics_txt_dash_pages')),
- 'col_page' => _t('_bx_analytics_txt_dash_col_page'),
- 'col_blocks' => _t('_bx_analytics_txt_dash_blocks'),
- 'col_neo' => _t('_bx_analytics_txt_dash_neo_blocks'),
- 'col_actions' => _t('_bx_analytics_txt_dash_col_actions'),
- 'bx_repeat:pages' => $aPages,
- ],
- ],
- 'bx_if:empty' => [
- 'condition' => empty($aPages),
- 'content' => ['empty' => _t('_bx_analytics_txt_dash_empty')],
- ],
- 'neo_note' => _t('_bx_analytics_txt_dash_neo_note'),
- 'create_title' => _t('_bx_analytics_txt_dash_create'),
- 'csrf' => $this->_esc((string)BxDolForm::getCsrfToken()),
- 'label_title' => _t('_bx_analytics_txt_dash_title'),
- 'create_hint' => _t('_bx_analytics_txt_dash_create_hint'),
- 'disable_warning' => _t('_bx_analytics_txt_dash_disable_warning'),
- 'create' => _t('_bx_analytics_txt_dash_create_btn'),
- ]);
- }
- /**
- * Studio > Setup: tracker status per platform, checks of the admin's own browser, what
- * each client needs, and custom events.
- *
- * @param array $aData status (see BxAnalyticsModule::getSetupStatus), tracking, injection,
- * track_actions, alerts, api_enabled, api_key, api_origin
- */
- public function getStudioSetup($aData)
- {
- $sRoot = BX_DOL_URL_ROOT;
- $sCsrf = $this->_esc((string)BxDolForm::getCsrfToken());
- // what NEO and the apps need from the site
- $aNeoChecks = [
- ['on' => $aData['api_enabled'], 'text' => _t('_bx_analytics_txt_setup_neo_api')],
- ['on' => $aData['api_key'] || $aData['api_origin'], 'text' => _t('_bx_analytics_txt_setup_neo_access')],
- ];
- foreach([BX_ANALYTICS_PLATFORM_NEXT_WEB, BX_ANALYTICS_PLATFORM_RN_IOS, BX_ANALYTICS_PLATFORM_RN_ANDROID] as $sPlatform) {
- $aStatus = $aData['status'][$sPlatform];
- $aNeoChecks[] = [
- 'on' => $aStatus['enabled'],
- 'text' => _t($aStatus['enabled'] ? '_bx_analytics_txt_setup_platform_on' : '_bx_analytics_txt_setup_platform_off', _t($this->_oConfig->getPlatforms()[$sPlatform])),
- ];
- }
- $aChecks = [];
- foreach($aNeoChecks as $aCheck)
- $aChecks[] = [
- 'check_state' => $aCheck['on'] ? 'yes' : 'no',
- 'check_icon' => $this->_getIconHtml($aCheck['on'] ? 'check' : 'x'),
- 'check_text' => $this->_esc($aCheck['text']),
- ];
- $aAlerts = [];
- foreach($aData['alerts'] as $sAlert)
- $aAlerts[] = ['alert' => $this->_esc($sAlert)];
- $sCurl = "curl -X POST '" . $sRoot . "api.php?r=" . $this->MODULE . "/collect' \\\n"
- . " -H 'Authorization: Bearer YOUR_API_KEY' \\\n"
- . " -H 'Content-Type: application/json' \\\n"
- . " -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\"}}]}'";
- $this->_addToolsAssets();
- return $this->_getToolsJsInit() . $this->parseHtmlByName('studio_setup.html', [
- 'bx_if:tracking_off' => [
- 'condition' => !$aData['tracking'],
- 'content' => [
- 'tracking_off' => _t('_bx_analytics_txt_setup_tracking_off'),
- 'settings_url' => $this->_esc(BX_DOL_URL_STUDIO . 'module.php?name=' . $this->MODULE . '&page=settings'),
- 'settings' => _t('_bx_analytics_txt_setup_open_settings'),
- ],
- ],
- 'status_title' => _t('_bx_analytics_txt_setup_status'),
- 'status' => $this->getSetupStatusHtml($aData['status']),
- 'check_title' => _t('_bx_analytics_txt_setup_check_title'),
- 'check_hint' => _t('_bx_analytics_txt_setup_check_hint'),
- 'csrf' => $sCsrf,
- 'check' => _t('_bx_analytics_txt_setup_check'),
- 'test' => _t('_bx_analytics_txt_setup_test'),
- 'test_warning' => _t('_bx_analytics_txt_setup_test_warning'),
- 'install_title' => _t('_bx_analytics_txt_setup_install'),
- 'core_web_title' => _t('_bx_analytics_platform_core_web'),
- 'core_web_text' => _t('_bx_analytics_txt_setup_core_web'),
- 'injection_state' => $aData['injection'] ? 'yes' : 'no',
- 'injection_icon' => $this->_getIconHtml($aData['injection'] ? 'check' : 'x'),
- 'injection' => _t($aData['injection'] ? '_bx_analytics_txt_setup_injection_on' : '_bx_analytics_txt_setup_injection_off'),
- 'core_web_exclusions' => _t('_bx_analytics_txt_setup_core_web_exclusions'),
- 'neo_title' => _t('_bx_analytics_txt_setup_neo_title'),
- 'neo_text' => _t('_bx_analytics_txt_setup_neo'),
- 'bx_repeat:neo_checks' => $aChecks,
- 'api_title' => _t('_bx_analytics_platform_api'),
- 'api_text' => _t('_bx_analytics_txt_setup_api'),
- 'code_curl' => $this->_getCodeHtml('bx-analytics-setup-curl', _t('_bx_analytics_txt_setup_curl'), $sCurl),
- 'core_title' => _t('_bx_analytics_platform_core'),
- 'core_text' => _t('_bx_analytics_txt_setup_core'),
- 'actions_state' => $aData['track_actions'] ? 'yes' : 'no',
- 'actions_icon' => $this->_getIconHtml($aData['track_actions'] ? 'check' : 'x'),
- 'actions' => _t($aData['track_actions'] ? '_bx_analytics_txt_setup_actions_on' : '_bx_analytics_txt_setup_actions_off'),
- 'alerts_label' => _t('_bx_analytics_txt_setup_alerts'),
- 'bx_repeat:alerts' => $aAlerts,
- 'events_title' => _t('_bx_analytics_txt_setup_events'),
- 'events_hint' => _t('_bx_analytics_txt_setup_events_hint'),
- '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'});"),
- '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' });"),
- 'code_server' => $this->_getCodeHtml('bx-analytics-setup-srv', _t('_bx_analytics_txt_setup_events_server'), "bx_srv('" . $this->MODULE . "', 'track', ['export_done', ['rows' => 120]]);"),
- 'events_warning' => _t('_bx_analytics_txt_setup_events_warning'),
- ]);
- }
- /**
- * The tracker status table, on the page and after a test event.
- */
- public function getSetupStatusHtml($aStatus)
- {
- $aPlatforms = $this->_oConfig->getPlatforms();
- $aRows = [];
- foreach($aStatus as $sPlatform => $aRow) {
- $bApp = $this->_oConfig->isAppPlatform($sPlatform);
- $aRows[] = [
- 'platform_title' => _t($aPlatforms[$sPlatform]),
- 'status' => $aRow['status'],
- 'status_icon' => $this->_getIconHtml($aRow['status']),
- 'status_title' => _t('_bx_analytics_txt_setup_status_' . $aRow['status']),
- 'last' => $aRow['last'] ? $this->formatValue('datetime', $aRow['last']) : _t('_bx_analytics_txt_setup_never'),
- 'hits' => $this->formatValue('number', $aRow['hits']),
- // the server's own hits keep no visits
- 'visitors' => $sPlatform == BX_ANALYTICS_PLATFORM_CORE ? _t('_bx_analytics_txt_not_available') : $this->formatValue('number', $aRow['visitors']),
- 'sessions' => $sPlatform == BX_ANALYTICS_PLATFORM_CORE ? _t('_bx_analytics_txt_not_available') : $this->formatValue('number', $aRow['sessions']),
- 'version' => $bApp && $aRow['app_version'] !== '' ? $this->_esc($aRow['app_version']) : ($bApp ? _t('_bx_analytics_txt_setup_none') : _t('_bx_analytics_txt_not_available')),
- ];
- }
- return $this->parseHtmlByName('setup_status.html', [
- 'caption' => $this->_esc(_t('_bx_analytics_txt_setup_status')),
- 'col_platform' => _t('_bx_analytics_txt_platform'),
- 'col_status' => _t('_bx_analytics_txt_setup_col_status'),
- 'col_last' => _t('_bx_analytics_txt_setup_last_hit'),
- 'col_hits' => _t('_bx_analytics_txt_setup_hits_24h'),
- 'col_visitors' => _t('_bx_analytics_txt_setup_visitors_24h'),
- 'col_sessions' => _t('_bx_analytics_txt_setup_sessions_7d'),
- 'col_version' => _t('_bx_analytics_txt_setup_app_version'),
- 'bx_repeat:rows' => $aRows,
- ]);
- }
- /**
- * The result of "Check this browser": tracked or not and why, and what the server sees.
- *
- * @param array $aCheck see BxAnalyticsCollector::check
- */
- public function getSetupCheckHtml($aCheck)
- {
- $bTracked = $aCheck['reasons'] == ['ok'];
- $aReasons = [];
- foreach($aCheck['reasons'] as $sReason)
- if($sReason != 'ok')
- $aReasons[] = [
- 'reason_kind' => 'no',
- 'reason_icon' => $this->_getIconHtml('x'),
- 'reason' => _t('_bx_analytics_txt_check_' . $sReason),
- ];
- foreach($aCheck['notes'] as $sNote)
- $aReasons[] = [
- 'reason_kind' => 'note',
- 'reason_icon' => $this->_getIconHtml('info'),
- 'reason' => _t('_bx_analytics_txt_check_' . $sNote),
- ];
- $sGeo = _t('_bx_analytics_txt_check_geo_none');
- if($aCheck['geo_source'] == BX_ANALYTICS_GEO_NONE)
- $sGeo = _t('_bx_analytics_txt_check_geo_off');
- elseif($aCheck['country_header'] !== '')
- $sGeo = _t('_bx_analytics_txt_check_geo_header', $this->_esc(str_replace('_', '-', preg_replace('/^HTTP_/', '', $aCheck['country_header']))), $this->_esc($aCheck['country']));
- return $this->parseHtmlByName('setup_check.html', [
- 'verdict_state' => $bTracked ? 'yes' : 'no',
- 'verdict_icon' => $this->_getIconHtml($bTracked ? 'check' : 'x'),
- 'verdict' => _t($bTracked ? '_bx_analytics_txt_check_tracked' : '_bx_analytics_txt_check_not_tracked'),
- 'bx_if:reasons' => [
- 'condition' => !empty($aReasons),
- 'content' => ['bx_repeat:reasons' => $aReasons],
- ],
- 'ip_label' => _t('_bx_analytics_txt_check_ip'),
- 'ip' => $aCheck['ip'] !== '' ? $this->_esc($aCheck['ip']) : _t('_bx_analytics_txt_not_available'),
- 'remote_label' => _t('_bx_analytics_txt_check_remote_addr'),
- 'remote' => $aCheck['remote_addr'] !== '' ? $this->_esc($aCheck['remote_addr']) : _t('_bx_analytics_txt_not_available'),
- 'geo_label' => _t('_bx_analytics_txt_check_geo'),
- 'geo' => $sGeo,
- 'identity_label' => _t('_bx_analytics_adm_stg_cpt_identity'),
- 'identity' => _t('_bx_analytics_identity_' . $aCheck['identity']),
- ]);
- }
- /**
- * A code snippet with a copy button. The code is text: it is escaped here.
- */
- protected function _getCodeHtml($sId, $sLabel, $sCode)
- {
- return $this->parseHtmlByName('code.html', [
- 'code_id' => $sId,
- 'code_label' => $this->_esc($sLabel),
- 'code' => $this->_esc($sCode),
- 'copy' => _t('_bx_analytics_txt_copy'),
- 'copied' => $this->_esc(_t('_bx_analytics_txt_copied')),
- ]);
- }
- /**
- * A small Lucide icon next to a status text, hidden from screen readers.
- *
- * @param string $sName receiving, quiet, nodata, off, check, x, info
- */
- protected function _getIconHtml($sName)
- {
- $aIcons = ['receiving' => 'check', 'quiet' => 'clock', 'nodata' => 'minus', 'off' => 'power', 'check' => 'check', 'x' => 'x', 'info' => 'info'];
- $sIcon = isset($aIcons[$sName]) ? $aIcons[$sName] : 'info';
- $aVars = [];
- foreach(array_unique(array_values($aIcons)) as $sGlyph)
- $aVars['bx_if:' . $sGlyph] = ['condition' => $sGlyph == $sIcon, 'content' => []];
- return $this->parseHtmlByName('icon.html', $aVars);
- }
- protected function _addToolsAssets()
- {
- $this->_addAssets(['studio_tools.js'], ['dashboard.css']);
- }
- protected function _getToolsJsInit()
- {
- return $this->getJsCode('tools', [
- 'aStrings' => [
- 'error' => _t('_bx_analytics_txt_error_loading'),
- 'running' => _t('_bx_analytics_txt_api_running'),
- 'meta' => _t('_bx_analytics_txt_api_meta', '{status}', '{ms}', '{size}'),
- 'truncated' => _t('_bx_analytics_txt_api_truncated'),
- 'bad_json' => _t('_bx_analytics_txt_api_bad_params'),
- ],
- ], [
- 'mask' => "if(typeof window.{object} == 'undefined') window.{object} = new {class}({params});",
- 'wrap' => true,
- ]);
- }
- /**
- * Creates the dashboard script object once per page, whatever block comes first.
- */
- protected function _getJsInit()
- {
- return $this->getJsCode('dashboard', [
- 'sActionUrl' => BX_DOL_URL_ROOT . $this->_oConfig->getBaseUri(),
- 'aStrings' => [
- 'duration_h' => _t('_bx_analytics_txt_duration_h'),
- 'duration_m' => _t('_bx_analytics_txt_duration_m'),
- 'duration_s' => _t('_bx_analytics_txt_duration_s'),
- 'ms' => _t('_bx_analytics_txt_ms'),
- 'error' => _t('_bx_analytics_txt_error_loading'),
- ],
- ], [
- 'mask' => "if(typeof window.{object} == 'undefined') window.{object} = new {class}({params});",
- 'wrap' => true,
- ]);
- }
- protected function _addDashboardAssets($sReport = '')
- {
- $aJs = ['chart.umd.min.js', 'dashboard.js'];
- $aCss = ['dashboard.css'];
- // the map uses the Leaflet copy and tile provider the site's location maps use
- if($sReport == 'map') {
- $aJs = array_merge(['leaflet/leaflet.js', 'leaflet-providers/leaflet-providers.js'], $aJs);
- $aCss[] = BX_DIRECTORY_PATH_PLUGINS_PUBLIC . 'leaflet/|leaflet.css';
- }
- $this->_addAssets($aJs, $aCss);
- }
- protected function _addAssets($aJs, $aCss)
- {
- if($this->_bStudio) {
- $this->addStudioJs($aJs);
- $this->addStudioCss($aCss);
- return;
- }
- $this->addJs($aJs);
- $this->addCss($aCss);
- }
- /**
- * 950, 9.5K, 95K, 1.2M
- */
- protected function _formatNumber($iValue)
- {
- $iValue = (int)$iValue;
- foreach([1000000000 => 'B', 1000000 => 'M', 1000 => 'K'] as $iSize => $sSuffix)
- if($iValue >= $iSize) {
- $fValue = $iValue / $iSize;
- return ($fValue < 10 ? rtrim(rtrim(number_format($fValue, 1), '0'), '.') : number_format(floor($fValue))) . $sSuffix;
- }
- return (string)$iValue;
- }
- protected function _formatChange($mixedChange)
- {
- if($mixedChange === null)
- return '';
- return ($mixedChange > 0 ? '+' : '') . number_format(abs($mixedChange) < 10 ? $mixedChange : round($mixedChange), abs($mixedChange) < 10 ? 1 : 0) . '%';
- }
- /**
- * good, bad or flat, where down is good for bounce rate.
- */
- protected function _trend($sMetric, $mixedChange)
- {
- if($mixedChange === null || $mixedChange == 0)
- return 'flat';
- $bUp = $mixedChange > 0;
- if(in_array($sMetric, ['bounce_rate', 'errors', 'refunds', 'cancelled', 'churn'], true))
- return $bUp ? 'bad' : 'good';
- return $bUp ? 'good' : 'bad';
- }
- /**
- * Visitor-supplied text (paths, titles, referrers) for HTML text and attributes.
- */
- protected function _esc($s)
- {
- return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
- }
- /**
- * JSON safe to put inside a script element.
- */
- protected function _json($a)
- {
- return json_encode($a, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE);
- }
- }
- /** @} */
|