1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9namespace Piwik; 10 11use Piwik\Cache as PiwikCache; 12use Piwik\Container\StaticContainer; 13 14require_once PIWIK_INCLUDE_PATH . "/core/Piwik.php"; 15 16/** 17 * This class contains metadata regarding core metrics and contains several 18 * related helper functions. 19 * 20 * Of note are the `INDEX_...` constants. In the database, metric column names 21 * in {@link DataTable} rows are stored as integers to save space. The integer 22 * values used are determined by these constants. 23 * 24 * @api 25 */ 26class Metrics 27{ 28 /* 29 * When saving DataTables in the DB, we replace all columns name with these IDs. This saves many bytes, 30 * eg. INDEX_NB_UNIQ_VISITORS is an integer: 4 bytes, but 'nb_uniq_visitors' is 16 bytes at least 31 */ 32 const INDEX_NB_UNIQ_VISITORS = 1; 33 const INDEX_NB_VISITS = 2; 34 const INDEX_NB_ACTIONS = 3; 35 const INDEX_MAX_ACTIONS = 4; 36 const INDEX_SUM_VISIT_LENGTH = 5; 37 const INDEX_BOUNCE_COUNT = 6; 38 const INDEX_NB_VISITS_CONVERTED = 7; 39 const INDEX_NB_CONVERSIONS = 8; 40 const INDEX_REVENUE = 9; 41 const INDEX_GOALS = 10; 42 const INDEX_SUM_DAILY_NB_UNIQ_VISITORS = 11; 43 44 // Specific to the Actions reports 45 const INDEX_PAGE_NB_HITS = 12; 46 const INDEX_PAGE_SUM_TIME_SPENT = 13; 47 const INDEX_PAGE_EXIT_NB_UNIQ_VISITORS = 14; 48 const INDEX_PAGE_EXIT_NB_VISITS = 15; 49 const INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS = 16; 50 const INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS = 17; 51 const INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS = 18; 52 const INDEX_PAGE_ENTRY_NB_VISITS = 19; 53 const INDEX_PAGE_ENTRY_NB_ACTIONS = 20; 54 const INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH = 21; 55 const INDEX_PAGE_ENTRY_BOUNCE_COUNT = 22; 56 57 // Ecommerce Items reports 58 const INDEX_ECOMMERCE_ITEM_REVENUE = 23; 59 const INDEX_ECOMMERCE_ITEM_QUANTITY = 24; 60 const INDEX_ECOMMERCE_ITEM_PRICE = 25; 61 const INDEX_ECOMMERCE_ORDERS = 26; 62 const INDEX_ECOMMERCE_ITEM_PRICE_VIEWED = 27; 63 64 // Site Search 65 const INDEX_SITE_SEARCH_HAS_NO_RESULT = 28; 66 const INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS = 29; 67 68 // Performance Analytics 69 const INDEX_PAGE_SUM_TIME_GENERATION = 30; 70 const INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION = 31; 71 const INDEX_PAGE_MIN_TIME_GENERATION = 32; 72 const INDEX_PAGE_MAX_TIME_GENERATION = 33; 73 74 // Events 75 const INDEX_EVENT_NB_HITS = 34; 76 const INDEX_EVENT_SUM_EVENT_VALUE = 35; 77 const INDEX_EVENT_MIN_EVENT_VALUE = 36; 78 const INDEX_EVENT_MAX_EVENT_VALUE = 37; 79 const INDEX_EVENT_NB_HITS_WITH_VALUE = 38; 80 81 // Number of unique User IDs 82 const INDEX_NB_USERS = 39; 83 const INDEX_SUM_DAILY_NB_USERS = 40; 84 85 // Contents 86 const INDEX_CONTENT_NB_IMPRESSIONS = 41; 87 const INDEX_CONTENT_NB_INTERACTIONS = 42; 88 89 // Unique visitors fingerprints (useful to process unique visitors across websites) 90 const INDEX_NB_UNIQ_FINGERPRINTS = 43; 91 92 // Goal reports 93 const INDEX_GOAL_NB_CONVERSIONS = 1; 94 const INDEX_GOAL_REVENUE = 2; 95 const INDEX_GOAL_NB_VISITS_CONVERTED = 3; 96 const INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL = 4; 97 const INDEX_GOAL_ECOMMERCE_REVENUE_TAX = 5; 98 const INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING = 6; 99 const INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT = 7; 100 const INDEX_GOAL_ECOMMERCE_ITEMS = 8; 101 102 public static $mappingFromIdToName = array( 103 Metrics::INDEX_NB_UNIQ_VISITORS => 'nb_uniq_visitors', 104 Metrics::INDEX_NB_UNIQ_FINGERPRINTS => 'nb_uniq_fingerprints', 105 Metrics::INDEX_NB_VISITS => 'nb_visits', 106 Metrics::INDEX_NB_ACTIONS => 'nb_actions', 107 Metrics::INDEX_NB_USERS => 'nb_users', 108 Metrics::INDEX_MAX_ACTIONS => 'max_actions', 109 Metrics::INDEX_SUM_VISIT_LENGTH => 'sum_visit_length', 110 Metrics::INDEX_BOUNCE_COUNT => 'bounce_count', 111 Metrics::INDEX_NB_VISITS_CONVERTED => 'nb_visits_converted', 112 Metrics::INDEX_NB_CONVERSIONS => 'nb_conversions', 113 Metrics::INDEX_REVENUE => 'revenue', 114 Metrics::INDEX_GOALS => 'goals', 115 Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_nb_uniq_visitors', 116 Metrics::INDEX_SUM_DAILY_NB_USERS => 'sum_daily_nb_users', 117 118 // Actions metrics 119 Metrics::INDEX_PAGE_NB_HITS => 'nb_hits', 120 Metrics::INDEX_PAGE_SUM_TIME_SPENT => 'sum_time_spent', 121 Metrics::INDEX_PAGE_SUM_TIME_GENERATION => 'sum_time_generation', 122 Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION => 'nb_hits_with_time_generation', 123 Metrics::INDEX_PAGE_MIN_TIME_GENERATION => 'min_time_generation', 124 Metrics::INDEX_PAGE_MAX_TIME_GENERATION => 'max_time_generation', 125 126 Metrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => 'exit_nb_uniq_visitors', 127 Metrics::INDEX_PAGE_EXIT_NB_VISITS => 'exit_nb_visits', 128 Metrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_exit_nb_uniq_visitors', 129 130 Metrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => 'entry_nb_uniq_visitors', 131 Metrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_entry_nb_uniq_visitors', 132 Metrics::INDEX_PAGE_ENTRY_NB_VISITS => 'entry_nb_visits', 133 Metrics::INDEX_PAGE_ENTRY_NB_ACTIONS => 'entry_nb_actions', 134 Metrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH => 'entry_sum_visit_length', 135 Metrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT => 'entry_bounce_count', 136 Metrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS => 'nb_hits_following_search', 137 138 // Items reports metrics 139 Metrics::INDEX_ECOMMERCE_ITEM_REVENUE => 'revenue', 140 Metrics::INDEX_ECOMMERCE_ITEM_QUANTITY => 'quantity', 141 Metrics::INDEX_ECOMMERCE_ITEM_PRICE => 'price', 142 Metrics::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED => 'price_viewed', 143 Metrics::INDEX_ECOMMERCE_ORDERS => 'orders', 144 145 // Events 146 Metrics::INDEX_EVENT_NB_HITS => 'nb_events', 147 Metrics::INDEX_EVENT_SUM_EVENT_VALUE => 'sum_event_value', 148 Metrics::INDEX_EVENT_MIN_EVENT_VALUE => 'min_event_value', 149 Metrics::INDEX_EVENT_MAX_EVENT_VALUE => 'max_event_value', 150 Metrics::INDEX_EVENT_NB_HITS_WITH_VALUE => 'nb_events_with_value', 151 152 // Contents 153 Metrics::INDEX_CONTENT_NB_IMPRESSIONS => 'nb_impressions', 154 Metrics::INDEX_CONTENT_NB_INTERACTIONS => 'nb_interactions' 155 ); 156 157 public static $mappingFromIdToNameGoal = array( 158 Metrics::INDEX_GOAL_NB_CONVERSIONS => 'nb_conversions', 159 Metrics::INDEX_GOAL_NB_VISITS_CONVERTED => 'nb_visits_converted', 160 Metrics::INDEX_GOAL_REVENUE => 'revenue', 161 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 'revenue_subtotal', 162 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 'revenue_tax', 163 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 'revenue_shipping', 164 Metrics::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 'revenue_discount', 165 Metrics::INDEX_GOAL_ECOMMERCE_ITEMS => 'items', 166 ); 167 168 protected static $metricsAggregatedFromLogs = array( 169 Metrics::INDEX_NB_UNIQ_VISITORS, 170 Metrics::INDEX_NB_VISITS, 171 Metrics::INDEX_NB_ACTIONS, 172 Metrics::INDEX_NB_USERS, 173 Metrics::INDEX_MAX_ACTIONS, 174 Metrics::INDEX_SUM_VISIT_LENGTH, 175 Metrics::INDEX_BOUNCE_COUNT, 176 Metrics::INDEX_NB_VISITS_CONVERTED, 177 ); 178 179 public static function getMappingFromIdToName() 180 { 181 $cache = PiwikCache::getTransientCache(); 182 $cacheKey = CacheId::siteAware(CacheId::pluginAware('Metrics.mappingFromIdToName')); 183 184 $value = $cache->fetch($cacheKey); 185 if (empty($value)) { 186 $value = self::$mappingFromIdToName; 187 188 /** 189 * Use this event if your plugin uses custom metric integer IDs to associate those IDs with the 190 * actual metric names (eg, 2 => nb_visits). This allows matomo to automate the replacing 191 * of IDs => metric names for your new metrics. 192 * 193 * **Example** 194 * 195 * public function addMetricIdToNameMapping(&$mapping) 196 * { 197 * $mapping[Archiver::INDEX_MY_NEW_METRIC] = $mapping['MyPlugin_myNewMetric']; 198 * } 199 * 200 * @ignore 201 */ 202 Piwik::postEvent('Metrics.addMetricIdToNameMapping', [&$value]); 203 204 $cache->save($cacheKey, $value); 205 } 206 return $value; 207 } 208 209 public static function getVisitsMetricNames() 210 { 211 $names = array(); 212 213 foreach (self::$metricsAggregatedFromLogs as $metricId) { 214 $names[$metricId] = self::$mappingFromIdToName[$metricId]; 215 } 216 217 return $names; 218 } 219 220 public static function getMappingFromNameToId() 221 { 222 static $nameToId = null; 223 if ($nameToId === null) { 224 $nameToId = array_flip(self::$mappingFromIdToName); 225 } 226 return $nameToId; 227 } 228 229 public static function getMappingFromNameToIdGoal() 230 { 231 static $nameToId = null; 232 if ($nameToId === null) { 233 $nameToId = array_flip(self::$mappingFromIdToNameGoal); 234 } 235 return $nameToId; 236 } 237 238 /** 239 * Is a lower value for a given column better? 240 * @param $column 241 * @return bool 242 * 243 * @ignore 244 */ 245 public static function isLowerValueBetter($column) 246 { 247 $isLowerBetter = null; 248 249 /** 250 * Use this event to define if a lower value of a metric is better. 251 * 252 * @param string $isLowerBetter should be set to a boolean indicating if lower is better 253 * @param string $column name of the column to determine 254 * 255 * **Example** 256 * 257 * public function checkIsLowerMetricValueBetter(&$isLowerBetter, $metric) 258 * { 259 * if ($metric === 'position') { 260 * $isLowerBetter = true; 261 * } 262 * } 263 */ 264 Piwik::postEvent('Metrics.isLowerValueBetter', [&$isLowerBetter, $column]); 265 266 if (!is_null($isLowerBetter)) { 267 return true; 268 } 269 270 $lowerIsBetterPatterns = array( 271 'bounce', 'exit' 272 ); 273 274 foreach ($lowerIsBetterPatterns as $pattern) { 275 if (strpos($column, $pattern) !== false) { 276 return true; 277 } 278 } 279 280 return false; 281 } 282 283 /** 284 * Derive the unit name from a column name 285 * @param $column 286 * @param $idSite 287 * @return string 288 * @ignore 289 */ 290 public static function getUnit($column, $idSite) 291 { 292 $nameToUnit = array( 293 '_rate' => '%', 294 'revenue' => Site::getCurrencySymbolFor($idSite), 295 '_time_' => 's' 296 ); 297 298 $unit = null; 299 300 /** 301 * Use this event to define units for custom metrics used in evolution graphs and row evolution only. 302 * 303 * @param string $unit should hold the unit (e.g. %, €, s or empty string) 304 * @param string $column name of the column to determine 305 * @param string $idSite id of the current site 306 */ 307 Piwik::postEvent('Metrics.getEvolutionUnit', [&$unit, $column, $idSite]); 308 309 if (!empty($unit)) { 310 return $unit; 311 } 312 313 foreach ($nameToUnit as $pattern => $type) { 314 if (strpos($column, $pattern) !== false) { 315 return $type; 316 } 317 } 318 319 return ''; 320 } 321 322 public static function getDefaultMetricTranslations() 323 { 324 $cacheId = CacheId::pluginAware('DefaultMetricTranslations'); 325 $cache = PiwikCache::getTransientCache(); 326 327 if ($cache->contains($cacheId)) { 328 return $cache->fetch($cacheId); 329 } 330 331 $translations = array( 332 'label' => 'General_ColumnLabel', 333 'date' => 'General_Date', 334 'avg_time_on_page' => 'General_ColumnAverageTimeOnPage', 335 'sum_time_spent' => 'General_ColumnSumVisitLength', 336 'sum_visit_length' => 'General_ColumnSumVisitLength', 337 'bounce_count' => 'General_ColumnBounces', 338 'bounce_count_returning' => 'VisitFrequency_ColumnBounceCountForReturningVisits', 339 'max_actions' => 'General_ColumnMaxActions', 340 'max_actions_returning' => 'VisitFrequency_ColumnMaxActionsInReturningVisit', 341 'nb_visits_converted_returning' => 'VisitFrequency_ColumnNbReturningVisitsConverted', 342 'sum_visit_length_returning' => 'VisitFrequency_ColumnSumVisitLengthReturning', 343 'nb_visits_converted' => 'General_ColumnVisitsWithConversions', 344 'nb_conversions' => 'Goals_ColumnConversions', 345 'revenue' => 'General_ColumnRevenue', 346 'nb_hits' => 'General_ColumnPageviews', 347 'entry_nb_visits' => 'General_ColumnEntrances', 348 'entry_nb_uniq_visitors' => 'General_ColumnUniqueEntrances', 349 'exit_nb_visits' => 'General_ColumnExits', 350 'exit_nb_uniq_visitors' => 'General_ColumnUniqueExits', 351 'entry_bounce_count' => 'General_ColumnBounces', 352 'exit_bounce_count' => 'General_ColumnBounces', 353 'exit_rate' => 'General_ColumnExitRate', 354 ); 355 356 $dailySum = ' (' . Piwik::translate('General_DailySum') . ')'; 357 $afterEntry = ' ' . Piwik::translate('General_AfterEntry'); 358 359 $translations['sum_daily_nb_uniq_visitors'] = Piwik::translate('General_ColumnNbUniqVisitors') . $dailySum; 360 $translations['sum_daily_nb_users'] = Piwik::translate('General_ColumnNbUsers') . $dailySum; 361 $translations['sum_daily_entry_nb_uniq_visitors'] = Piwik::translate('General_ColumnUniqueEntrances') . $dailySum; 362 $translations['sum_daily_exit_nb_uniq_visitors'] = Piwik::translate('General_ColumnUniqueExits') . $dailySum; 363 $translations['entry_nb_actions'] = Piwik::translate('General_ColumnNbActions') . $afterEntry; 364 $translations['entry_sum_visit_length'] = Piwik::translate('General_ColumnSumVisitLength') . $afterEntry; 365 366 $translations = array_merge(self::getDefaultMetrics(), self::getDefaultProcessedMetrics(), $translations); 367 368 /** 369 * Use this event to register translations for metrics processed by your plugin. 370 * 371 * @param string $translations The array mapping of column_name => Plugin_TranslationForColumn 372 */ 373 Piwik::postEvent('Metrics.getDefaultMetricTranslations', array(&$translations)); 374 375 $translations = array_map(array('\\Piwik\\Piwik', 'translate'), $translations); 376 377 $cache->save($cacheId, $translations); 378 379 return $translations; 380 } 381 382 public static function getDefaultMetrics() 383 { 384 $cacheId = CacheId::languageAware('DefaultMetrics'); 385 $cache = PiwikCache::getTransientCache(); 386 387 if ($cache->contains($cacheId)) { 388 return $cache->fetch($cacheId); 389 } 390 391 $translations = array( 392 'nb_visits' => 'General_ColumnNbVisits', 393 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitors', 394 'nb_actions' => 'General_ColumnNbActions', 395 'nb_users' => 'General_ColumnNbUsers', 396 ); 397 $translations = array_map(array('\\Piwik\\Piwik', 'translate'), $translations); 398 399 $cache->save($cacheId, $translations); 400 401 return $translations; 402 } 403 404 public static function getDefaultProcessedMetrics() 405 { 406 $cacheId = CacheId::languageAware('DefaultProcessedMetrics'); 407 $cache = PiwikCache::getTransientCache(); 408 409 if ($cache->contains($cacheId)) { 410 return $cache->fetch($cacheId); 411 } 412 413 $translations = array( 414 // Processed in AddColumnsProcessedMetrics 415 'nb_actions_per_visit' => 'General_ColumnActionsPerVisit', 416 'avg_time_on_site' => 'General_ColumnAvgTimeOnSite', 417 'bounce_rate' => 'General_ColumnBounceRate', 418 'conversion_rate' => 'General_ColumnConversionRate', 419 ); 420 $translations = array_map(array('\\Piwik\\Piwik', 'translate'), $translations); 421 422 $cache->save($cacheId, $translations); 423 424 return $translations; 425 } 426 427 public static function getReadableColumnName($columnIdRaw) 428 { 429 $mappingIdToName = self::$mappingFromIdToName; 430 431 if (array_key_exists($columnIdRaw, $mappingIdToName)) { 432 return $mappingIdToName[$columnIdRaw]; 433 } 434 435 return $columnIdRaw; 436 } 437 438 public static function getMetricIdsToProcessReportTotal() 439 { 440 return array( 441 self::INDEX_NB_VISITS, 442 self::INDEX_NB_UNIQ_VISITORS, 443 self::INDEX_NB_ACTIONS, 444 self::INDEX_PAGE_NB_HITS, 445 self::INDEX_NB_VISITS_CONVERTED, 446 self::INDEX_NB_CONVERSIONS, 447 self::INDEX_BOUNCE_COUNT, 448 self::INDEX_PAGE_ENTRY_BOUNCE_COUNT, 449 self::INDEX_PAGE_ENTRY_NB_VISITS, 450 self::INDEX_PAGE_ENTRY_NB_ACTIONS, 451 self::INDEX_PAGE_EXIT_NB_VISITS, 452 self::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS, 453 self::INDEX_REVENUE 454 ); 455 } 456 457 public static function getDefaultMetricsDocumentation() 458 { 459 $cacheId = CacheId::pluginAware('DefaultMetricsDocumentation'); 460 $cache = PiwikCache::getTransientCache(); 461 462 if ($cache->contains($cacheId)) { 463 return $cache->fetch($cacheId); 464 } 465 466 $translations = array( 467 'nb_visits' => 'General_ColumnNbVisitsDocumentation', 468 'nb_uniq_visitors' => 'General_ColumnNbUniqVisitorsDocumentation', 469 'nb_actions' => 'General_ColumnNbActionsDocumentation', 470 'nb_users' => 'General_ColumnNbUsersDocumentation', 471 'nb_actions_per_visit' => 'General_ColumnActionsPerVisitDocumentation', 472 'avg_time_on_site' => 'General_ColumnAvgTimeOnSiteDocumentation', 473 'bounce_rate' => 'General_ColumnBounceRateDocumentation', 474 'conversion_rate' => 'General_ColumnConversionRateDocumentation', 475 'avg_time_on_page' => 'General_ColumnAverageTimeOnPageDocumentation', 476 'nb_hits' => 'General_ColumnPageviewsDocumentation', 477 'exit_rate' => 'General_ColumnExitRateDocumentation' 478 ); 479 480 /** 481 * Use this event to register translations for metrics documentation processed by your plugin. 482 * 483 * @param string[] $translations The array mapping of column_name => Plugin_TranslationForColumnDocumentation 484 */ 485 Piwik::postEvent('Metrics.getDefaultMetricDocumentationTranslations', array(&$translations)); 486 487 $translations = array_map(array('\\Piwik\\Piwik', 'translate'), $translations); 488 489 $cache->save($cacheId, $translations); 490 491 return $translations; 492 } 493 494 public static function getPercentVisitColumn() 495 { 496 $percentVisitsLabel = str_replace(' ', ' ', Piwik::translate('General_ColumnPercentageVisits')); 497 return $percentVisitsLabel; 498 } 499} 500