language key. Codes are stored with every session and * event, so they never change; a new client is a new code. * - core-web: the classic UNA web UI (PHP and HTML) * - next-web, rn-ios, rn-android: NEO on the web (Next.js) and its React Native apps * - api: any other client calling api.php directly * - core: the server itself, with no client request behind it (cron, background jobs) */ protected $_aPlatforms = [ BX_ANALYTICS_PLATFORM_CORE_WEB => '_bx_analytics_platform_core_web', BX_ANALYTICS_PLATFORM_NEXT_WEB => '_bx_analytics_platform_next_web', BX_ANALYTICS_PLATFORM_RN_IOS => '_bx_analytics_platform_rn_ios', BX_ANALYTICS_PLATFORM_RN_ANDROID => '_bx_analytics_platform_rn_android', BX_ANALYTICS_PLATFORM_API => '_bx_analytics_platform_api', BX_ANALYTICS_PLATFORM_CORE => '_bx_analytics_platform_core', ]; /** * Platforms that run in a browser, where the user agent tells the browser and the device. */ protected $_aBrowserPlatforms = [ BX_ANALYTICS_PLATFORM_CORE_WEB, BX_ANALYTICS_PLATFORM_NEXT_WEB, ]; /** * Native apps, which report their system and device themselves. */ protected $_aAppPlatforms = [ BX_ANALYTICS_PLATFORM_RN_IOS, BX_ANALYTICS_PLATFORM_RN_ANDROID, ]; function __construct($aModule) { parent::__construct($aModule); $this->CNF = [ 'TABLE_SESSIONS' => $aModule['db_prefix'] . 'sessions', 'TABLE_EVENTS' => $aModule['db_prefix'] . 'events', 'TABLE_SALTS' => $aModule['db_prefix'] . 'salts', 'TABLE_LIMITS' => $aModule['db_prefix'] . 'limits', 'TABLE_BOTS' => $aModule['db_prefix'] . 'bots', 'TABLE_BOT_PAGES' => $aModule['db_prefix'] . 'bot_pages', 'TABLE_GOALS' => $aModule['db_prefix'] . 'goals', 'TABLE_FUNNELS' => $aModule['db_prefix'] . 'funnels', 'TABLE_REVENUE' => $aModule['db_prefix'] . 'revenue', 'TABLE_SUBSCRIPTIONS' => $aModule['db_prefix'] . 'subscriptions', 'TABLE_ERRORS' => $aModule['db_prefix'] . 'errors', 'TABLE_ERROR_HITS' => $aModule['db_prefix'] . 'error_hits', 'TABLE_VITALS' => $aModule['db_prefix'] . 'vitals', // import of past payments: the cursors, rows read per service call, and the time // one step may take in the hourly cron and from the Studio button 'OPTION_BACKFILL' => 'bx_analytics_revenue_backfill', 'OPTION_BACKFILL_SBS' => 'bx_analytics_revenue_backfill_sbs', 'BACKFILL_PAGE' => 500, 'BACKFILL_SECONDS' => 10, // funnel steps at most 'MAX_FUNNEL_STEPS' => 6, 'MAX_GOALS' => 20, // Query: values drawn on the chart, and rows in a CSV download at most 'QUERY_SERIES_VALUES' => 5, 'MAX_EXPORT_ROWS' => 10000, // report data is cached this many seconds per report, filters, parameters, // language and kind of viewer (live reports aren't); goal, funnel and error // status changes drop the copies 'REPORT_CACHE_TTL' => 180, 'REPORT_CACHE_PREFIX' => 'bx_analytics_report_', // hits per request and their properties, to bound the work of one beacon, and the // largest request body read at all (20 hits, 10 of them errors with long stacks) 'MAX_HITS_PER_REQUEST' => 20, 'MAX_PROPS_LENGTH' => 2048, 'MAX_COLLECT_BYTES' => 256 * 1024, // health: error hits per request and per visit, new error groups per hour (more go // to one catch-all group), how old a deferred error may be (an app that crashed // sends it on the next launch), and vitals hits per request 'MAX_ERRORS_PER_REQUEST' => 10, 'MAX_ERRORS_PER_SESSION' => 50, 'MAX_NEW_ERROR_GROUPS_PER_HOUR' => 200, 'MAX_ERROR_AGE' => 7 * 86400, 'MAX_VITALS_PER_REQUEST' => 1, // vitals thresholds, metric => [good up to, poor above]: milliseconds, and CLS in // thousandths (the web.dev and Lighthouse values; app screens and starts by RN guides) 'VITALS' => [ 'lcp' => [2500, 4000], 'inp' => [200, 500], 'cls' => [100, 250], 'fcp' => [1800, 3000], 'ttfb' => [800, 1800], 'screen_load' => [1000, 2500], 'app_start' => [2000, 4000], ], // engaged time above this on one page is a tab left open, not reading 'MAX_ENGAGED_MS' => 30 * 60 * 1000, // rows deleted per query when pruning, and the time one pruning run may take 'PRUNING_BATCH' => 5000, 'PRUNING_SECONDS' => 20, ]; $this->_aJsClasses = [ 'analytics' => 'BxAnalytics', 'dashboard' => 'BxAnalyticsDashboard', 'tools' => 'BxAnalyticsStudioTools', ]; $this->_aJsObjects = [ 'analytics' => 'oAnalytics', 'dashboard' => 'oBxAnalyticsDashboard', 'tools' => 'oBxAnalyticsStudioTools', ]; } public function getPlatforms() { return $this->_aPlatforms; } public function isPlatform($sPlatform) { return is_string($sPlatform) && isset($this->_aPlatforms[$sPlatform]); } public function isBrowserPlatform($sPlatform) { return in_array($sPlatform, $this->_aBrowserPlatforms, true); } public function isAppPlatform($sPlatform) { return in_array($sPlatform, $this->_aAppPlatforms, true); } public function isTrackingEnabled() { return getParam('bx_analytics_tracking') == 'on'; } public function isPlatformEnabled($sPlatform) { return $this->isPlatform($sPlatform) && in_array($sPlatform, $this->_getList('bx_analytics_platforms'), true); } public function getSessionTimeout() { return 60 * max(1, (int)getParam('bx_analytics_session_timeout')); } /** * Hits the public collector takes from one address in a minute, 0 for no limit. */ public function getLimitHitsPerMinute() { return max(0, (int)getParam('bx_analytics_limit_hits')); } /** * Visits that may start from one address in an hour, 0 for no limit. */ public function getLimitVisitsPerHour() { return max(0, (int)getParam('bx_analytics_limit_visits')); } public function getOnlineWindow() { return 60 * max(1, (int)getParam('bx_analytics_online_window')); } public function getIdentityMode() { return getParam('bx_analytics_identity') == BX_ANALYTICS_IDENTITY_PERSISTENT ? BX_ANALYTICS_IDENTITY_PERSISTENT : BX_ANALYTICS_IDENTITY_COOKIELESS; } public function isLinkMembers() { return getParam('bx_analytics_link_members') == 'on'; } public function isTrackActions() { return getParam('bx_analytics_track_actions') == 'on'; } public function isTrackBots() { return getParam('bx_analytics_track_bots') == 'on'; } public function isRespectDnt() { return getParam('bx_analytics_respect_dnt') == 'on'; } public function getExcludedLevels() { return array_map('intval', $this->_getList('bx_analytics_exclude_levels')); } public function getExcludedIps() { return $this->_getList('bx_analytics_exclude_ips'); } public function getExcludedPaths() { return $this->_getList('bx_analytics_exclude_paths'); } /** * Query parameters left out of stored paths, as patterns with * for any characters. */ public function getIgnoredParams() { return $this->_getList('bx_analytics_ignore_params'); } public function getTrustedProxies() { return $this->_getList('bx_analytics_trusted_proxies'); } /** * Where the visitor's location comes from: none, the headers of any CDN (the first one * found), or the headers of one CDN only (the others can be sent by the client, since an * edge sets its own headers and passes the rest through). */ public function getGeoSource() { $sSource = getParam('bx_analytics_geo_source'); return in_array($sSource, [BX_ANALYTICS_GEO_HEADERS, BX_ANALYTICS_GEO_CLOUDFLARE, BX_ANALYTICS_GEO_VERCEL, BX_ANALYTICS_GEO_CLOUDFRONT], true) ? $sSource : BX_ANALYTICS_GEO_NONE; } /** * The CDN whose location headers are read ('' for any), or false when the location isn't * read at all. * * @return string|false */ public function getGeoEdge() { $sSource = $this->getGeoSource(); if($sSource == BX_ANALYTICS_GEO_NONE) return false; return $sSource == BX_ANALYTICS_GEO_HEADERS ? '' : $sSource; } public function getRetentionDays() { return max(1, (int)getParam('bx_analytics_retention_days')); } public function isTrackErrors() { return getParam('bx_analytics_track_errors') == 'on'; } public function isTrackVitals() { return getParam('bx_analytics_track_vitals') == 'on'; } /** * Percent of page loads that measure vitals, 0 to 100; the client samples. */ public function getVitalsSample() { return min(100, max(0, (int)getParam('bx_analytics_vitals_sample'))); } /** * Error messages that are not stored, one pattern per line with * for any characters. * Lines, not commas: messages have spaces and commas. */ public function getErrorsIgnore() { $sValue = trim((string)getParam('bx_analytics_errors_ignore')); if($sValue === '') return []; $aLines = preg_split('/\R/u', $sValue); if($aLines === false) return []; return array_values(array_filter(array_map('trim', $aLines), 'get_mb_len')); } /** * Vitals are kept for this many days, at most as long as the rest. */ public function getVitalsRetentionDays() { return min($this->getRetentionDays(), max(1, (int)getParam('bx_analytics_vitals_retention_days'))); } /** * Hosts that belong to the site: a referrer from one of them isn't a traffic source. */ public function getInternalHosts() { $aHosts = [parse_url(BX_DOL_URL_ROOT, PHP_URL_HOST)]; /** * @hooks * @hookdef hook-bx_analytics-get_internal_hosts 'bx_analytics', 'get_internal_hosts' - hook to add the hosts of other clients (NEO web, white-label domains) whose referrers are internal * - $unit_name - equals `bx_analytics` * - $action - equals `get_internal_hosts` * - $object_id - not used * - $sender_id - not used * - $extra_params - array of additional params with the following array keys: * - `hosts` - [array] by ref, list of host names, can be overridden in hook processing * @hook @ref hook-bx_analytics-get_internal_hosts */ bx_alert($this->getName(), 'get_internal_hosts', 0, 0, [ 'hosts' => &$aHosts, ]); return array_filter($aHosts); } /** * Values of a list setting, which Studio stores comma separated; text settings may use lines too. */ protected function _getList($sName) { $sValue = trim((string)getParam($sName)); if($sValue === '') return []; return array_values(array_filter(array_map('trim', preg_split('/[\s,]+/', $sValue)), 'get_mb_len')); } } /** @} */