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(' ', '&nbsp;', Piwik::translate('General_ColumnPercentageVisits'));
497        return $percentVisitsLabel;
498    }
499}
500