diff --git a/.gitattributes b/.gitattributes index 5ffd723ef8caa3f9d5c2be75e188d4ab0545747d..1dd80d6c4daa310b102321570b717a818d2619f4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -86,3 +86,5 @@ tests/javascript/assets/** linguist-vendored tests/javascript/frameworks/** linguist-vendored tests/javascript/jash/** linguist-vendored tests/javascript/jslint/** linguist-vendored +plugins/AnonymousPiwikUsageMeasurement/screenshots/AdminSettings.png filter=lfs diff=lfs merge=lfs -text +plugins/Bandwidth/screenshots/Visitor_Overview.png filter=lfs diff=lfs merge=lfs -text diff --git a/plugins/Actions/Metrics.php b/plugins/Actions/Metrics.php new file mode 100644 index 0000000000000000000000000000000000000000..0e8907b897a07fcc006265a0a68224ac2d7f3d40 --- /dev/null +++ b/plugins/Actions/Metrics.php @@ -0,0 +1,103 @@ + PiwikMetrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => PiwikMetrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS, + PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => PiwikMetrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS, + ); + + public static $columnsToDeleteAfterAggregation = array( + PiwikMetrics::INDEX_NB_UNIQ_VISITORS, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS, + PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS, + ); + + public static function getColumnsAggregationOperation() + { + $operations = []; + $actionMetrics = self::getActionMetrics(); + + foreach ($actionMetrics as $actionMetric => $definition) { + if (!empty($definition['aggregation']) && $definition['aggregation'] !== 'sum') { + $operations[$actionMetric] = $definition['aggregation']; + } + } + + return $operations; + } + + public static function getActionMetrics() + { + $metricsConfig = array( + PiwikMetrics::INDEX_NB_VISITS => array( + 'aggregation' => 'sum', + 'query' => "count(distinct log_link_visit_action.idvisit)", + ), + PiwikMetrics::INDEX_NB_UNIQ_VISITORS => array( + 'aggregation' => false, + 'query' => "count(distinct log_link_visit_action.idvisitor)", + ), + PiwikMetrics::INDEX_PAGE_NB_HITS => array( + 'aggregation' => 'sum', + 'query' => "count(*)", + ), + PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION => array( + 'aggregation' => 'sum', + 'query' => "sum( + case when " . Action::DB_COLUMN_CUSTOM_FLOAT . " is null + then 0 + else " . Action::DB_COLUMN_CUSTOM_FLOAT . " + end + ) / 1000", + ), + PiwikMetrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION => array( + 'aggregation' => 'sum', + 'query' => "sum( + case when " . Action::DB_COLUMN_CUSTOM_FLOAT . " is null + then 0 + else 1 + end + )", + ), + PiwikMetrics::INDEX_PAGE_MIN_TIME_GENERATION => array( + 'aggregation' => 'min', + 'query' => "min(" . Action::DB_COLUMN_CUSTOM_FLOAT . ") / 1000", + ), + PiwikMetrics::INDEX_PAGE_MAX_TIME_GENERATION => array( + 'aggregation' => 'max', + 'query' => "max(" . Action::DB_COLUMN_CUSTOM_FLOAT . ") / 1000", + ), + ); + + Piwik::postEvent('Actions.Archiving.addActionMetrics', array(&$metricsConfig)); + + return $metricsConfig; + } +} diff --git a/plugins/Actions/RecordBuilders/ActionReports.php b/plugins/Actions/RecordBuilders/ActionReports.php new file mode 100644 index 0000000000000000000000000000000000000000..4caeafeb6d414c83e9b5f0f3b5bd0ef31d8eb9db --- /dev/null +++ b/plugins/Actions/RecordBuilders/ActionReports.php @@ -0,0 +1,982 @@ +setBlobColumnAggregationOps(Metrics::getColumnsAggregationOperation()); + $pageTitlesRecord = Record::make(Record::TYPE_BLOB, Archiver::PAGE_TITLES_RECORD_NAME) + ->setBlobColumnAggregationOps(Metrics::getColumnsAggregationOperation()); + + if ($this->isFlatArchivingEnabled()) { + $this->setHierarchyBuiltFromFlatRecord( + $pageUrlsRecord, + Archiver::PAGE_URLS_FLAT_RECORD_NAME, + [$this, 'flatRowToUrlHierarchyPath'], + [$this, 'reduceLegacyUrlHierarchyIntoFlatTable'] + ); + $this->setHierarchyBuiltFromFlatRecord( + $pageTitlesRecord, + Archiver::PAGE_TITLES_FLAT_RECORD_NAME, + [$this, 'flatRowToTitleHierarchyPath'], + [$this, 'reduceLegacyTitleHierarchyIntoFlatTable'] + ); + } + + $records = [ + Record::make(Record::TYPE_BLOB, Archiver::SITE_SEARCH_RECORD_NAME) + ->setMaxRowsInTable(ArchivingHelper::$maximumRowsInDataTableSiteSearch), + + $pageUrlsRecord, + $pageTitlesRecord, + + Record::make(Record::TYPE_BLOB, Archiver::DOWNLOADS_RECORD_NAME), + Record::make(Record::TYPE_BLOB, Archiver::OUTLINKS_RECORD_NAME), + + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_SEARCHES_RECORD_NAME), + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_KEYWORDS_RECORD_NAME) + ->setIsCountOfBlobRecordRows(Archiver::SITE_SEARCH_RECORD_NAME), + + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_OUTLINKS_RECORD_NAME), + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_UNIQ_OUTLINKS_RECORD_NAME), + + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_PAGEVIEWS_RECORD_NAME), + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME), + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_SUM_TIME_RECORD_NAME), + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_HITS_TIMED_RECORD_NAME), + + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_DOWNLOADS_RECORD_NAME), + Record::make(Record::TYPE_NUMERIC, Archiver::METRIC_UNIQ_DOWNLOADS_RECORD_NAME), + ]; + + if ($this->isFlatArchivingEnabled()) { + $records[] = Record::make(Record::TYPE_BLOB, Archiver::PAGE_URLS_FLAT_RECORD_NAME) + ->setBlobColumnAggregationOps(Metrics::getColumnsAggregationOperation()) + ->setMaxRowsInTable(ArchivingHelper::$maximumRowsInDataTableFlat); + $records[] = Record::make(Record::TYPE_BLOB, Archiver::PAGE_TITLES_FLAT_RECORD_NAME) + ->setBlobColumnAggregationOps(Metrics::getColumnsAggregationOperation()) + ->setMaxRowsInTable(ArchivingHelper::$maximumRowsInDataTableFlat); + } + + return $records; + } + + public function flatRowToUrlHierarchyPath(Row $flatRow): ?array + { + return $this->flatRowToHierarchyPath($flatRow, Action::TYPE_PAGE_URL); + } + + public function flatRowToTitleHierarchyPath(Row $flatRow): ?array + { + return $this->flatRowToHierarchyPath($flatRow, Action::TYPE_PAGE_TITLE); + } + + public function reduceLegacyUrlHierarchyIntoFlatTable(DataTable $legacyHierarchy, DataTable $flatTable, ArchiveProcessor $archiveProcessor, Record $record): void + { + ArchivingHelper::mergeHierarchicalActionsTableIntoBestEffortFlatTable( + $legacyHierarchy, + $flatTable, + Action::TYPE_PAGE_URL + ); + } + + public function reduceLegacyTitleHierarchyIntoFlatTable(DataTable $legacyHierarchy, DataTable $flatTable, ArchiveProcessor $archiveProcessor, Record $record): void + { + ArchivingHelper::mergeHierarchicalActionsTableIntoBestEffortFlatTable( + $legacyHierarchy, + $flatTable, + Action::TYPE_PAGE_TITLE + ); + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + if (!$this->isFlatArchivingEnabled()) { + return $this->aggregateLegacyHierarchical($archiveProcessor); + } + + $rankingQueryLimit = ArchivingHelper::getRankingQueryLimit(); + ArchivingHelper::reloadConfig(); + + $tablesByType = $this->makeReportTables(); + $flatPageTablesByType = $this->makeFlatPageTables(); + $tableModesByType = [ + Action::TYPE_PAGE_URL => ArchivingHelper::ACTION_TABLE_MODE_FLAT, + Action::TYPE_PAGE_TITLE => ArchivingHelper::ACTION_TABLE_MODE_FLAT, + ]; + $dayTablesByType = $tablesByType; + $dayTablesByType[Action::TYPE_PAGE_URL] = $flatPageTablesByType[Action::TYPE_PAGE_URL]; + $dayTablesByType[Action::TYPE_PAGE_TITLE] = $flatPageTablesByType[Action::TYPE_PAGE_TITLE]; + + $this->archiveDayActions( + $archiveProcessor, + $rankingQueryLimit, + $dayTablesByType, + array_diff(array_keys($dayTablesByType), [Action::TYPE_SITE_SEARCH]), + true, + $tableModesByType + ); + + if ($archiveProcessor->getParams()->getSite()->isSiteSearchEnabled()) { + $rankingQueryLimitSiteSearch = max($rankingQueryLimit, ArchivingHelper::$maximumRowsInDataTableSiteSearch); + $this->archiveDayActions( + $archiveProcessor, + $rankingQueryLimitSiteSearch, + $dayTablesByType, + [Action::TYPE_SITE_SEARCH], + false, + $tableModesByType + ); + } + + $this->archiveDayEntryActions($archiveProcessor->getLogAggregator(), $dayTablesByType, $rankingQueryLimit, $tableModesByType); + $this->archiveDayExitActions($archiveProcessor->getLogAggregator(), $dayTablesByType, $rankingQueryLimit, $tableModesByType); + $this->archiveDayActionsTime($archiveProcessor->getLogAggregator(), $dayTablesByType, $rankingQueryLimit, $tableModesByType); + $this->archiveDayActionsGoals($archiveProcessor, $rankingQueryLimit); + ArchivingHelper::normalizeFlatGoalsMetricsForHierarchy($flatPageTablesByType); + + ArchivingHelper::clearActionsCache(); + + $tablesByType[Action::TYPE_PAGE_URL] = $this->buildDayHierarchicalTableFromFlatTable( + $flatPageTablesByType[Action::TYPE_PAGE_URL], + [$this, 'flatRowToUrlHierarchyPath'] + ); + $tablesByType[Action::TYPE_PAGE_TITLE] = $this->buildDayHierarchicalTableFromFlatTable( + $flatPageTablesByType[Action::TYPE_PAGE_TITLE], + [$this, 'flatRowToTitleHierarchyPath'] + ); + $this->finalizeBuiltFromFlatHierarchyTable( + $archiveProcessor, + $tablesByType[Action::TYPE_PAGE_URL], + Archiver::PAGE_URLS_RECORD_NAME + ); + $this->finalizeBuiltFromFlatHierarchyTable( + $archiveProcessor, + $tablesByType[Action::TYPE_PAGE_TITLE], + Archiver::PAGE_TITLES_RECORD_NAME + ); + + $dataTable = $tablesByType[Action::TYPE_SITE_SEARCH]; + $this->deleteUnusedColumnsFromKeywordsDataTable($dataTable); + + foreach ($tablesByType as $actionType => $dataTable) { + if ( + $actionType === Action::TYPE_PAGE_URL + || $actionType === Action::TYPE_PAGE_TITLE + ) { + continue; + } + + ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($dataTable); + } + foreach ($flatPageTablesByType as $flatDataTable) { + // also removes unique visitor columns from the summary row, as those cannot + // be summed and would otherwise be aggregated into non-day periods + ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($flatDataTable); + } + + $nbSearches = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)); + $nbKeywords = $dataTable->getRowsCount(); + + $dataTable = $tablesByType[Action::TYPE_OUTLINK]; + $nbOutlinks = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)); + $nbUniqOutlinks = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS)); + + $dataTable = $flatPageTablesByType[Action::TYPE_PAGE_URL]; + $nbPageviews = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)); + $nbUniqPageviews = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS)); + $nbSumTimeGeneration = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION)); + $nbHitsWithTimeGeneration = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION)); + + $dataTable = $tablesByType[Action::TYPE_DOWNLOAD]; + $nbDownloads = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)); + $nbUniqDownloads = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS)); + + return [ + // blob records + Archiver::PAGE_URLS_FLAT_RECORD_NAME => $flatPageTablesByType[Action::TYPE_PAGE_URL], + Archiver::PAGE_TITLES_FLAT_RECORD_NAME => $flatPageTablesByType[Action::TYPE_PAGE_TITLE], + Archiver::PAGE_URLS_RECORD_NAME => $tablesByType[Action::TYPE_PAGE_URL], + Archiver::PAGE_TITLES_RECORD_NAME => $tablesByType[Action::TYPE_PAGE_TITLE], + Archiver::DOWNLOADS_RECORD_NAME => $tablesByType[Action::TYPE_DOWNLOAD], + Archiver::OUTLINKS_RECORD_NAME => $tablesByType[Action::TYPE_OUTLINK], + Archiver::SITE_SEARCH_RECORD_NAME => $tablesByType[Action::TYPE_SITE_SEARCH], + + // numeric records + Archiver::METRIC_SEARCHES_RECORD_NAME => $nbSearches, + Archiver::METRIC_KEYWORDS_RECORD_NAME => $nbKeywords, + + Archiver::METRIC_OUTLINKS_RECORD_NAME => $nbOutlinks, + Archiver::METRIC_UNIQ_OUTLINKS_RECORD_NAME => $nbUniqOutlinks, + + Archiver::METRIC_PAGEVIEWS_RECORD_NAME => $nbPageviews, + Archiver::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME => $nbUniqPageviews, + Archiver::METRIC_SUM_TIME_RECORD_NAME => $nbSumTimeGeneration, + Archiver::METRIC_HITS_TIMED_RECORD_NAME => $nbHitsWithTimeGeneration, + + Archiver::METRIC_DOWNLOADS_RECORD_NAME => $nbDownloads, + Archiver::METRIC_UNIQ_DOWNLOADS_RECORD_NAME => $nbUniqDownloads, + ]; + } + + protected function beforeInsertBuiltFromFlatHierarchyRecord( + ArchiveProcessor $archiveProcessor, + Record $hierarchicalRecord, + DataTable $hierarchicalTable, + DataTable $flatTable + ): void { + $recordName = $hierarchicalRecord->getName(); + if ( + $recordName !== Archiver::PAGE_URLS_RECORD_NAME + && $recordName !== Archiver::PAGE_TITLES_RECORD_NAME + ) { + return; + } + + $this->finalizeBuiltFromFlatHierarchyTable($archiveProcessor, $hierarchicalTable, $recordName); + } + + private function flatRowToHierarchyPath(Row $flatRow, int $actionType): ?array + { + $label = $flatRow->getColumn('label'); + if (!is_string($label) || $label === '') { + return null; + } + + if ($actionType === Action::TYPE_PAGE_URL) { + if ($label === ArchivingHelper::getUnknownActionName(Action::TYPE_PAGE_URL)) { + return [$label]; + } + + return ArchivingHelper::getActionExplodedNames( + ArchivingHelper::removePageUrlLeafMarkerFromFlatLabel($label), + $actionType + ); + } + + return ArchivingHelper::getActionExplodedNames($label, $actionType); + } + + private function removeFlatPathMetadataFromDataTable(DataTable $dataTable): void + { + $summaryRow = $dataTable->getRowFromId(DataTable::ID_SUMMARY_ROW); + if ($summaryRow instanceof Row) { + $summaryRow->deleteMetadata(ArchivingHelper::ACTION_FLAT_PATH_METADATA_NAME); + } + + foreach ($dataTable->getRows() as $row) { + $row->deleteMetadata(ArchivingHelper::ACTION_FLAT_PATH_METADATA_NAME); + + $subtable = $row->getSubtable(); + if ($subtable instanceof DataTable) { + $this->removeFlatPathMetadataFromDataTable($subtable); + } + } + } + + private function finalizeBuiltFromFlatHierarchyTable( + ArchiveProcessor $archiveProcessor, + DataTable $hierarchicalTable, + string $recordName + ): void { + $hierarchicalTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, Metrics::getColumnsAggregationOperation()); + + if ($recordName === Archiver::PAGE_URLS_RECORD_NAME) { + $prefix = $archiveProcessor->getParams()->getSite()->getMainUrl(); + $prefix = rtrim($prefix, '/') . '/'; + ArchivingHelper::setFolderPathMetadata($hierarchicalTable, $isUrl = true, $prefix); + } elseif ($recordName === Archiver::PAGE_TITLES_RECORD_NAME) { + ArchivingHelper::setFolderPathMetadata($hierarchicalTable, $isUrl = false); + } + + $this->removeFlatPathMetadataFromDataTable($hierarchicalTable); + ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($hierarchicalTable); + } + + private function isFlatArchivingEnabled(): bool + { + ArchivingHelper::reloadConfig(); + return ArchivingHelper::$maximumRowsInDataTableFlat > 0; + } + + private function setHierarchyBuiltFromFlatRecord( + Record $record, + string $flatRecordName, + callable $flatToHierarchyPathCallback, + callable $legacyHierarchyToFlatReducer + ): void { + $record->setBuiltFromFlatRecord( + $flatRecordName, + $flatToHierarchyPathCallback, + $legacyHierarchyToFlatReducer + )->setMaxRowsInTable(0)->setMaxRowsInSubtable(0); + } + + private function buildDayHierarchicalTableFromFlatTable( + DataTable $flatTable, + callable $flatToHierarchyPathCallback + ): DataTable { + return $this->buildHierarchicalTableFromFlatTable( + $flatTable, + Metrics::getColumnsAggregationOperation(), + $flatToHierarchyPathCallback, + $this->getDefaultHierarchyRowColumns() + ); + } + + private function getDefaultHierarchyRowColumns(): array + { + return [ + PiwikMetrics::INDEX_NB_VISITS => 0, + PiwikMetrics::INDEX_NB_UNIQ_VISITORS => 0, + PiwikMetrics::INDEX_PAGE_NB_HITS => 0, + PiwikMetrics::INDEX_PAGE_SUM_TIME_SPENT => 0, + ]; + } + + private function aggregateLegacyHierarchical(ArchiveProcessor $archiveProcessor): array + { + $rankingQueryLimit = ArchivingHelper::getRankingQueryLimit(); + ArchivingHelper::reloadConfig(); + + $tablesByType = $this->makeReportTables(); + + $this->archiveDayActions( + $archiveProcessor, + $rankingQueryLimit, + $tablesByType, + array_diff(array_keys($tablesByType), [Action::TYPE_SITE_SEARCH]), + true + ); + + if ($archiveProcessor->getParams()->getSite()->isSiteSearchEnabled()) { + $rankingQueryLimitSiteSearch = max($rankingQueryLimit, ArchivingHelper::$maximumRowsInDataTableSiteSearch); + $this->archiveDayActions($archiveProcessor, $rankingQueryLimitSiteSearch, $tablesByType, [Action::TYPE_SITE_SEARCH], false); + } + + $this->archiveDayEntryActions($archiveProcessor->getLogAggregator(), $tablesByType, $rankingQueryLimit); + $this->archiveDayExitActions($archiveProcessor->getLogAggregator(), $tablesByType, $rankingQueryLimit); + $this->archiveDayActionsTime($archiveProcessor->getLogAggregator(), $tablesByType, $rankingQueryLimit); + $this->archiveDayActionsGoals($archiveProcessor, $rankingQueryLimit); + + ArchivingHelper::clearActionsCache(); + + $prefix = $archiveProcessor->getParams()->getSite()->getMainUrl(); + $prefix = rtrim($prefix, '/') . '/'; + ArchivingHelper::setFolderPathMetadata($tablesByType[Action::TYPE_PAGE_URL], $isUrl = true, $prefix); + ArchivingHelper::setFolderPathMetadata($tablesByType[Action::TYPE_PAGE_TITLE], $isUrl = false); + + $dataTable = $tablesByType[Action::TYPE_SITE_SEARCH]; + $this->deleteUnusedColumnsFromKeywordsDataTable($dataTable); + + foreach ($tablesByType as $dataTable) { + ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($dataTable); + } + + $nbSearches = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)); + $nbKeywords = $dataTable->getRowsCount(); + + $dataTable = $tablesByType[Action::TYPE_OUTLINK]; + $nbOutlinks = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)); + $nbUniqOutlinks = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS)); + + $dataTable = $tablesByType[Action::TYPE_PAGE_URL]; + $nbPageviews = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)); + $nbUniqPageviews = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS)); + $nbSumTimeGeneration = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_SUM_TIME_GENERATION)); + $nbHitsWithTimeGeneration = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION)); + + $dataTable = $tablesByType[Action::TYPE_DOWNLOAD]; + $nbDownloads = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_PAGE_NB_HITS)); + $nbUniqDownloads = array_sum($dataTable->getColumn(PiwikMetrics::INDEX_NB_VISITS)); + + return [ + Archiver::PAGE_URLS_RECORD_NAME => $tablesByType[Action::TYPE_PAGE_URL], + Archiver::PAGE_TITLES_RECORD_NAME => $tablesByType[Action::TYPE_PAGE_TITLE], + Archiver::DOWNLOADS_RECORD_NAME => $tablesByType[Action::TYPE_DOWNLOAD], + Archiver::OUTLINKS_RECORD_NAME => $tablesByType[Action::TYPE_OUTLINK], + Archiver::SITE_SEARCH_RECORD_NAME => $tablesByType[Action::TYPE_SITE_SEARCH], + + Archiver::METRIC_SEARCHES_RECORD_NAME => $nbSearches, + Archiver::METRIC_KEYWORDS_RECORD_NAME => $nbKeywords, + Archiver::METRIC_OUTLINKS_RECORD_NAME => $nbOutlinks, + Archiver::METRIC_UNIQ_OUTLINKS_RECORD_NAME => $nbUniqOutlinks, + Archiver::METRIC_PAGEVIEWS_RECORD_NAME => $nbPageviews, + Archiver::METRIC_UNIQ_PAGEVIEWS_RECORD_NAME => $nbUniqPageviews, + Archiver::METRIC_SUM_TIME_RECORD_NAME => $nbSumTimeGeneration, + Archiver::METRIC_HITS_TIMED_RECORD_NAME => $nbHitsWithTimeGeneration, + Archiver::METRIC_DOWNLOADS_RECORD_NAME => $nbDownloads, + Archiver::METRIC_UNIQ_DOWNLOADS_RECORD_NAME => $nbUniqDownloads, + ]; + } + + protected function deleteUnusedColumnsFromKeywordsDataTable(DataTable $dataTable): void + { + $columnsToDelete = array( + PiwikMetrics::INDEX_NB_UNIQ_VISITORS, + PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS, + PiwikMetrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS, + PiwikMetrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT, + PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS, + ); + $dataTable->deleteColumns($columnsToDelete); + } + + private function makeReportTables(): array + { + $result = []; + foreach (Metrics::$actionTypes as $type) { + $dataTable = new DataTable(); + if ($type === Action::TYPE_SITE_SEARCH) { + $maxRows = ArchivingHelper::$maximumRowsInDataTableSiteSearch; + } else { + $maxRows = ArchivingHelper::$maximumRowsInDataTableLevelZero; + } + $dataTable->setMaximumAllowedRows($maxRows); + + if ( + $type == Action::TYPE_PAGE_URL + || $type == Action::TYPE_PAGE_TITLE + ) { + // for page urls and page titles, performance metrics exist and have to be aggregated correctly + $dataTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, Metrics::getColumnsAggregationOperation()); + } + + $result[$type] = $dataTable; + } + return $result; + } + + private function makeFlatPageTables(): array + { + $tables = []; + $maxRowsInFlatTable = ArchivingHelper::$maximumRowsInDataTableFlat; + foreach ([Action::TYPE_PAGE_URL, Action::TYPE_PAGE_TITLE] as $type) { + $table = new DataTable(); + $table->setMaximumAllowedRows($maxRowsInFlatTable); + $table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, Metrics::getColumnsAggregationOperation()); + $tables[$type] = $table; + } + + return $tables; + } + + protected function archiveDayActions( + ArchiveProcessor $archiveProcessor, + int $rankingQueryLimit, + array $actionsTablesByType, + $actionTypes, + bool $includePageNotDefined, + array $tableModesByType = [] + ): void { + $logAggregator = $archiveProcessor->getLogAggregator(); + + $metricsConfig = Metrics::getActionMetrics(); + + $select = "log_action.name, + log_action.type, + log_action.idaction, + log_action.url_prefix"; + + $select = $this->addMetricsToSelect($select, $metricsConfig); + + $from = array( + "log_link_visit_action", + array( + "table" => "log_action", + "joinOn" => "log_link_visit_action.%s = log_action.idaction", + ), + ); + + $where = $logAggregator->getWhereStatement('log_link_visit_action', 'server_time'); + $where .= " AND log_link_visit_action.%s IS NOT NULL" + . $this->getWhereClauseActionIsNotEvent(); + + $actionTypesWhere = "log_action.type IN (" . implode(", ", $actionTypes) . ")"; + if ($includePageNotDefined) { + $actionTypesWhere = "(" . $actionTypesWhere . " OR log_action.type IS NULL)"; + } + $where .= " AND $actionTypesWhere"; + + $groupBy = "log_link_visit_action.%s"; + $orderBy = "`" . PiwikMetrics::INDEX_PAGE_NB_HITS . "` DESC, name ASC"; + + $siteSearchEnabled = $archiveProcessor->getParams()->getSite()->isSiteSearchEnabled(); + + $rankingQuery = false; + if ($rankingQueryLimit > 0) { + $rankingQuery = new RankingQuery($rankingQueryLimit); + $rankingQuery->addLabelColumn(array('idaction', 'name')); + $rankingQuery->addColumn('url_prefix'); + + if ($siteSearchEnabled) { + $rankingQuery->addColumn(PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT, 'min'); + $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS, 'sum'); + } + + $this->addMetricsToRankingQuery($rankingQuery, $metricsConfig); + + $rankingQuery->partitionResultIntoMultipleGroups('type', $actionTypes); + } + + // Special Magic to get + // 1) No result Keywords + // 2) For each page view, count number of times the referrer page was a Site Search + if ($siteSearchEnabled) { + $this->updateQuerySelectFromForSiteSearch($select, $from); + } + + $this->archiveDayQueryProcess( + $logAggregator, + $actionsTablesByType, + $select, + $from, + $where, + $groupBy, + $orderBy, + "idaction_name", + $rankingQuery, + $metricsConfig, + $tableModesByType + ); + $this->archiveDayQueryProcess( + $logAggregator, + $actionsTablesByType, + $select, + $from, + $where, + $groupBy, + $orderBy, + "idaction_url", + $rankingQuery, + $metricsConfig, + $tableModesByType + ); + } + + /** + * Entry actions for Page URLs and Page names + */ + protected function archiveDayEntryActions(LogAggregator $logAggregator, array $actionsTablesByType, int $rankingQueryLimit, array $tableModesByType = []) + { + $rankingQuery = false; + if ($rankingQueryLimit > 0) { + $rankingQuery = new RankingQuery($rankingQueryLimit); + $rankingQuery->addLabelColumn('idaction'); + $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS); + $rankingQuery->addColumn(array(PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS, + PiwikMetrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH, + PiwikMetrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT), 'sum'); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($actionsTablesByType)); + + $extraSelects = 'log_action.type, log_action.name,'; + $from = array( + "log_visit", + array( + "table" => "log_action", + "joinOn" => "log_visit.%s = log_action.idaction", + ), + ); + $orderBy = "`" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS . "` DESC, log_action.name ASC"; + } else { + $extraSelects = false; + $from = "log_visit"; + $orderBy = false; + } + + $select = "log_visit.%s as idaction, $extraSelects + count(distinct log_visit.idvisitor) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS . "`, + count(*) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_VISITS . "`, + sum(log_visit.visit_total_actions) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_NB_ACTIONS . "`, + sum(log_visit.visit_total_time) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH . "`, + sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `" . PiwikMetrics::INDEX_PAGE_ENTRY_BOUNCE_COUNT . "`"; + + $where = $logAggregator->getWhereStatement('log_visit', 'visit_last_action_time'); + $where .= " AND log_visit.%s > 0"; + + $groupBy = "log_visit.%s"; + + $this->archiveDayQueryProcess( + $logAggregator, + $actionsTablesByType, + $select, + $from, + $where, + $groupBy, + $orderBy, + "visit_entry_idaction_url", + $rankingQuery, + [], + $tableModesByType + ); + + $this->archiveDayQueryProcess( + $logAggregator, + $actionsTablesByType, + $select, + $from, + $where, + $groupBy, + $orderBy, + "visit_entry_idaction_name", + $rankingQuery, + [], + $tableModesByType + ); + } + + /** + * Exit actions + */ + protected function archiveDayExitActions(LogAggregator $logAggregator, array $actionsTablesByType, int $rankingQueryLimit, array $tableModesByType = []) + { + $rankingQuery = false; + if ($rankingQueryLimit > 0) { + $rankingQuery = new RankingQuery($rankingQueryLimit); + $rankingQuery->addLabelColumn('idaction'); + $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS); + $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS, 'sum'); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($actionsTablesByType)); + + $extraSelects = 'log_action.type, log_action.name,'; + $from = array( + "log_visit", + array( + "table" => "log_action", + "joinOn" => "log_visit.%s = log_action.idaction", + ), + ); + $orderBy = "`" . PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS . "` DESC, log_action.name ASC"; + } else { + $extraSelects = false; + $from = "log_visit"; + $orderBy = false; + } + + $select = "log_visit.%s as idaction, $extraSelects + count(distinct log_visit.idvisitor) as `" . PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS . "`, + count(*) as `" . PiwikMetrics::INDEX_PAGE_EXIT_NB_VISITS . "`"; + + $where = $logAggregator->getWhereStatement('log_visit', 'visit_last_action_time'); + $where .= " AND log_visit.%s > 0"; + + $groupBy = "log_visit.%s"; + + $this->archiveDayQueryProcess( + $logAggregator, + $actionsTablesByType, + $select, + $from, + $where, + $groupBy, + $orderBy, + "visit_exit_idaction_url", + $rankingQuery, + [], + $tableModesByType + ); + + $this->archiveDayQueryProcess( + $logAggregator, + $actionsTablesByType, + $select, + $from, + $where, + $groupBy, + $orderBy, + "visit_exit_idaction_name", + $rankingQuery, + [], + $tableModesByType + ); + + return array($rankingQuery, $extraSelects, $from, $orderBy, $select, $where, $groupBy); + } + + /** + * Time per action + */ + protected function archiveDayActionsTime(LogAggregator $logAggregator, array $actionsTablesByType, int $rankingQueryLimit, array $tableModesByType = []) + { + $rankingQuery = false; + if ($rankingQueryLimit > 0) { + $rankingQuery = new RankingQuery($rankingQueryLimit); + $rankingQuery->addLabelColumn('idaction'); + $rankingQuery->addColumn(PiwikMetrics::INDEX_PAGE_SUM_TIME_SPENT, 'sum'); + $rankingQuery->partitionResultIntoMultipleGroups('type', array_keys($actionsTablesByType)); + + $extraSelects = "log_action.type, log_action.name, count(*) as `" . PiwikMetrics::INDEX_PAGE_NB_HITS . "`,"; + $from = array( + "log_link_visit_action", + array( + "table" => "log_action", + "joinOn" => "log_link_visit_action.%s = log_action.idaction", + ), + ); + $orderBy = "`" . PiwikMetrics::INDEX_PAGE_NB_HITS . "` DESC, log_action.name ASC"; + } else { + $extraSelects = false; + $from = "log_link_visit_action"; + $orderBy = false; + } + + $select = "log_link_visit_action.%s as idaction, $extraSelects + sum(log_link_visit_action.time_spent_ref_action) as `" . PiwikMetrics::INDEX_PAGE_SUM_TIME_SPENT . "`"; + + $where = $logAggregator->getWhereStatement('log_link_visit_action', 'server_time'); + $where .= " AND log_link_visit_action.time_spent_ref_action > 0 + AND log_link_visit_action.%s > 0" + . $this->getWhereClauseActionIsNotEvent(); + + $groupBy = "log_link_visit_action.%s"; + + $this->archiveDayQueryProcess( + $logAggregator, + $actionsTablesByType, + $select, + $from, + $where, + $groupBy, + $orderBy, + "idaction_url_ref", + $rankingQuery, + [], + $tableModesByType + ); + + $this->archiveDayQueryProcess( + $logAggregator, + $actionsTablesByType, + $select, + $from, + $where, + $groupBy, + $orderBy, + "idaction_name_ref", + $rankingQuery, + [], + $tableModesByType + ); + } + + protected function archiveDayQueryProcess( + LogAggregator $logAggregator, + array $actionsTablesByType, + string $select, + $from, + string $where, + string $groupBy, + $orderBy, + string $sprintfField, + ?RankingQuery $rankingQuery = null, + array $metricsConfig = array(), + array $tableModesByType = [] + ): void { + $select = sprintf($select, $sprintfField); + + // get query with segmentation + $query = $logAggregator->generateQuery($select, $from, $where, $groupBy, $orderBy); + + // replace the rest of the %s + $querySql = str_replace("%s", $sprintfField, $query['sql']); + + // apply ranking query + if ($rankingQuery) { + $querySql = $rankingQuery->generateRankingQuery($querySql); + } + + // get result + $resultSet = $logAggregator->getDb()->query($querySql, $query['bind']); + ArchivingHelper::updateActionsTableWithRowQuery( + $resultSet, + $sprintfField, + $actionsTablesByType, + $metricsConfig, + $tableModesByType + ); + } + + protected function updateQuerySelectFromForSiteSearch(string &$select, array &$from): void + { + $selectFlagNoResultKeywords = ", + MAX(log_link_visit_action.search_count) = 0 AS `" . PiwikMetrics::INDEX_SITE_SEARCH_HAS_NO_RESULT . "`"; + + // we need an extra JOIN to know whether the referrer "idaction_name_ref" was a Site Search request + $from[] = array( + "table" => "log_action", + "tableAlias" => "log_action_name_ref", + "joinOn" => "log_link_visit_action.idaction_name_ref = log_action_name_ref.idaction", + ); + + $selectPageIsFollowingSiteSearch = ", + SUM( CASE WHEN log_action_name_ref.type = " . Action::TYPE_SITE_SEARCH . " + THEN 1 ELSE 0 END) + AS `" . PiwikMetrics::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS . "`"; + + $select .= $selectFlagNoResultKeywords + . $selectPageIsFollowingSiteSearch; + } + + private function addMetricsToRankingQuery(RankingQuery $rankingQuery, array $metricsConfig): void + { + foreach ($metricsConfig as $metric => $config) { + if (!empty($config['aggregation'])) { + $rankingQuery->addColumn($metric, $config['aggregation']); + } else { + $rankingQuery->addColumn($metric); + } + } + } + + private function addMetricsToSelect(string $select, array $metricsConfig): string + { + if (!empty($metricsConfig)) { + foreach ($metricsConfig as $metric => $config) { + $select .= ', ' . $config['query'] . " as `" . $metric . "`"; + } + } + + return $select; + } + + protected function getWhereClauseActionIsNotEvent(): string + { + return " AND log_link_visit_action.idaction_event_category IS NULL"; + } + + /** + * Add goals data for each combination of url / title and pageviews / entries + */ + protected function archiveDayActionsGoals(ArchiveProcessor $archiveProcessor, int $rankingQueryLimit): void + { + $site = $archiveProcessor->getParams()->getSite(); + + if ( + !\Piwik\Common::isGoalPluginEnabled() || + GeneralConfig::getConfigValue('disable_archive_actions_goals', $site->getId()) + ) { + return; + } + + $goals = $this->getGoalsForSite($site->getId()); + + // Add orders and abandoned cart codes if the site is enabled for ecommerce + if ($site->isEcommerceEnabled()) { + $goals[] = GoalManager::IDGOAL_CART; + $goals[] = GoalManager::IDGOAL_ORDER; + } + + foreach ($goals as $idGoal) { + $this->archiveDayActionsGoalsPages($archiveProcessor->getLogAggregator(), true, $idGoal); + $this->archiveDayActionsGoalsPages($archiveProcessor->getLogAggregator(), false, $idGoal); + } + + $this->archiveDayActionsGoalsPagesEntry($archiveProcessor, $rankingQueryLimit, true); + $this->archiveDayActionsGoalsPagesEntry($archiveProcessor, $rankingQueryLimit, false); + } + + /** + * Query goal page view data and update actions data table + * + * @param bool $isUrl If true then query goal data by url, else by name + * @param int $idGoal Goal to archive + * + * @return int|null Count of records processed + * @throws \Exception + */ + protected function archiveDayActionsGoalsPages(LogAggregator $logAggregator, bool $isUrl, int $idGoal): ?int + { + $linkField = ($isUrl ? 'idaction_url' : 'idaction_name'); + $resultSet = $logAggregator->queryConversionsByPageView($linkField, $idGoal); + if (!$resultSet) { + return null; + } + return ArchivingHelper::updateActionsTableWithGoals($resultSet, true); + } + + /** + * Get a list of goal ids for a site + * + * @return array + */ + private function getGoalsForSite(string $idSite): array + { + $cache = Cache::getTransientCache(); + $key = 'ActionArchives_allGoalIds_' . $idSite; + + if ($cache->contains($key)) { + return $cache->fetch($key); + } + + $siteGoals = Request::processRequest('Goals.getGoals', ['idSite' => $idSite, 'filter_limit' => '-1'], $default = []); + $goalIds = array_column($siteGoals, 'idgoal'); + + $cache->save($key, $goalIds); + return $goalIds; + } + + /** + * Query goal entry page data and update actions data table + * + * @param bool $isUrl If true then query goal data by url, else by name + * + * @return int|null Count of records processed + * @throws \Exception + */ + protected function archiveDayActionsGoalsPagesEntry(ArchiveProcessor $archiveProcessor, int $rankingQueryLimit, bool $isUrl): ?int + { + if (GeneralConfig::getConfigValue('disable_archive_actions_goals', $archiveProcessor->getParams()->getSite()->getId())) { + return null; + } + $linkField = ($isUrl ? 'visit_entry_idaction_url' : 'visit_entry_idaction_name'); + $resultSet = $archiveProcessor->getLogAggregator()->queryConversionsByEntryPageView($linkField, $rankingQueryLimit); + if (!$resultSet) { + return null; + } + + return ArchivingHelper::updateActionsTableWithGoals($resultSet, false); + } +} diff --git a/plugins/Actions/RecordBuilders/Hits.php b/plugins/Actions/RecordBuilders/Hits.php new file mode 100644 index 0000000000000000000000000000000000000000..80b0a38c2b0766753b34091cff469837945b5d9e --- /dev/null +++ b/plugins/Actions/RecordBuilders/Hits.php @@ -0,0 +1,42 @@ +getLogAggregator()->queryActionsByDimension( + [], + '', + ['count(distinct log_link_visit_action.idlink_va) as `' . PiwikMetrics::INDEX_NB_HITS . '`'], + [], + null + ); + $data = $query->fetch(); + $nbHits = $data[PiwikMetrics::INDEX_NB_HITS]; + + return [ + Archiver::METRIC_HITS_RECORD_NAME => $nbHits, + ]; + } +} diff --git a/plugins/Actions/RecordBuilders/SearchCategories.php b/plugins/Actions/RecordBuilders/SearchCategories.php new file mode 100644 index 0000000000000000000000000000000000000000..240074d6b70fc35c637e124efc4765c04a0627fa --- /dev/null +++ b/plugins/Actions/RecordBuilders/SearchCategories.php @@ -0,0 +1,52 @@ +getLogAggregator(); + $query = $logAggregator->queryActionsByDimension(['search_cat'], "%s.search_cat != '' AND %s.search_cat IS NOT NULL"); + + $table = new DataTable(); + + while ($row = $query->fetch()) { + $label = $row['search_cat']; + unset($row['search_cat']); + + $row = new DataTable\Row([DataTable\Row::COLUMNS => ['label' => $label] + $row]); + $table->addRow($row); + } + + return [ + Archiver::SITE_SEARCH_CATEGORY_RECORD_NAME => $table, + ]; + } + + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + return [ + Record::make(Record::TYPE_BLOB, Archiver::SITE_SEARCH_CATEGORY_RECORD_NAME) + ->setMaxRowsInTable(ArchivingHelper::$maximumRowsInDataTableSiteSearch), + ]; + } + + public function isEnabled(ArchiveProcessor $archiveProcessor): bool + { + return $archiveProcessor->getParams()->getSite()->isSiteSearchEnabled(); + } +} diff --git a/plugins/Actions/Reports/Base.php b/plugins/Actions/Reports/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..79c13a0cc13887d8e53159ae0d1b47e2a4bc84cd --- /dev/null +++ b/plugins/Actions/Reports/Base.php @@ -0,0 +1,159 @@ +categoryId = 'General_Actions'; + $this->processedMetrics = []; + $this->recursiveLabelSeparator = '/'; + } + + protected function addBaseDisplayProperties(ViewDataTable $view) + { + $view->config->datatable_js_type = 'ActionsDataTable'; + $view->config->search_recursive = true; + $view->config->show_table_all_columns = false; + $view->requestConfig->filter_limit = Actions::ACTIONS_REPORT_ROWS_DISPLAY; + $view->config->show_all_views_icons = false; + + if ($view->requestConfig->getRequestParam('performance') === '1') { + $view->requestConfig->filter_limit = 25; + // hide visualization selector + $view->config->footer_icons = [[ + 'class' => 'tableAllColumnsSwitch', + 'buttons' => [], + ]]; + } + + if ($view->isViewDataTableId(HtmlTable::ID)) { + $view->config->show_embedded_subtable = true; + } + + if (Request::shouldLoadExpanded()) { + if ($view->isViewDataTableId(HtmlTable::ID)) { + $view->config->show_expanded = true; + } + + $view->config->filters[] = function ($dataTable) { + Actions::setDataTableRowLevels($dataTable); + }; + } + + $view->config->filters[] = function ($dataTable) use ($view) { + if ($view->isViewDataTableId(HtmlTable::ID)) { + $view->config->datatable_css_class = 'dataTableActions'; + } + }; + } + + protected function addPageDisplayProperties(ViewDataTable $view) + { + $view->config->addTranslations(array( + 'nb_hits' => Piwik::translate('General_ColumnPageviews'), + 'nb_visits' => Piwik::translate('General_ColumnUniquePageviews'), + )); + + $formatter = new Formatter(); + + // add avg_generation_time tooltip + $tooltipCallback = function ($hits, $min, $max) use ($formatter) { + if (!$hits) { + return false; + } + + return Piwik::translate("Actions_AvgGenerationTimeTooltip", array( + $hits, + "
", + $formatter->getPrettyTimeFromSeconds($min, true), + $formatter->getPrettyTimeFromSeconds($max, true), + )); + }; + $view->config->filters[] = array('ColumnCallbackAddMetadata', + array( + array('nb_hits_with_time_generation', 'min_time_generation', 'max_time_generation'), + 'avg_time_generation_tooltip', + $tooltipCallback, + ), + ); + + $this->addExcludeLowPopDisplayProperties($view); + + // hide the performance columns viz in page reports when not displayed as widget + if ($view->requestConfig->getRequestParam('widget') != '1') { + $view->config->show_table_performance = false; + } + } + + protected function addExcludeLowPopDisplayProperties(ViewDataTable $view) + { + if (Common::getRequestVar('enable_filter_excludelowpop', '0', 'string') != '0') { + if (Common::getRequestVar('flat', 0, 'int') === 1) { + $view->requestConfig->filter_excludelowpop = 'nb_hits'; + } else { + $view->requestConfig->filter_excludelowpop = Metrics::INDEX_PAGE_NB_HITS; + } + $view->requestConfig->filter_excludelowpop_value = function () { + // computing minimum value to exclude (2 percent of the total number of actions) + $visitsInfo = \Piwik\Plugins\VisitsSummary\Controller::getVisitsSummary()->getFirstRow(); + $nbActions = $visitsInfo->getColumn('nb_actions'); + $nbActionsLowPopulationThreshold = floor(0.02 * $nbActions); + + // we remove 1 to make sure some actions/downloads are displayed in the case we have a very few of them + // and each of them has 1 or 2 hits... + return min($visitsInfo->getColumn('max_actions') - 1, $nbActionsLowPopulationThreshold - 1); + }; + } + } + + public function getRecursiveLabelSeparator() + { + $legacyDelimiter = GeneralConfig::getConfigValue('action_category_delimiter'); + if (!empty($legacyDelimiter)) { + return $legacyDelimiter; + } + + if ( + $this->dimension instanceof PageTitle + || $this->dimension instanceof EntryPageTitle + || $this->dimension instanceof ExitPageTitle + ) { + return GeneralConfig::getConfigValue('action_title_category_delimiter') ?: $this->recursiveLabelSeparator; + } + + if ( + $this->dimension instanceof PageUrl + || $this->dimension instanceof EntryPageUrl + || $this->dimension instanceof ExitPageUrl + ) { + return GeneralConfig::getConfigValue('action_url_category_delimiter') ?: $this->recursiveLabelSeparator; + } + + return parent::getRecursiveLabelSeparator(); + } +} diff --git a/plugins/Actions/Reports/Get.php b/plugins/Actions/Reports/Get.php new file mode 100644 index 0000000000000000000000000000000000000000..8f5e7479e631a70588f9f922d59d5234f151b7c4 --- /dev/null +++ b/plugins/Actions/Reports/Get.php @@ -0,0 +1,39 @@ +name = Piwik::translate('General_Actions') . ' - ' . Piwik::translate('General_MainMetrics'); + $this->documentation = Piwik::translate('Actions_MainMetricsReportDocumentation'); + $this->order = 1; + $this->processedMetrics = array( + new AveragePageGenerationTime(), + ); + $this->metrics = array( + 'nb_pageviews', + 'nb_uniq_pageviews', + 'nb_downloads', + 'nb_uniq_downloads', + 'nb_outlinks', + 'nb_uniq_outlinks', + 'nb_searches', + 'nb_keywords', + 'hits', + ); + } +} diff --git a/plugins/Actions/Reports/GetDownloads.php b/plugins/Actions/Reports/GetDownloads.php new file mode 100644 index 0000000000000000000000000000000000000000..ca5c2143da5a8e343e79069834e78b2cc5d18e4a --- /dev/null +++ b/plugins/Actions/Reports/GetDownloads.php @@ -0,0 +1,56 @@ +dimension = new DownloadUrl(); + $this->name = Piwik::translate('General_Downloads'); + $this->documentation = Piwik::translate('Actions_DownloadsReportDocumentation', '
'); + $this->metrics = array('nb_visits', 'nb_hits'); + + $this->actionToLoadSubTables = $this->action; + $this->order = 9; + + $this->subcategoryId = 'General_Downloads'; + } + + public function getMetrics() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnUniqueDownloads'), + 'nb_hits' => Piwik::translate('General_Downloads'), + ); + } + + protected function getMetricsDocumentation() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicksDocumentation'), + 'nb_hits' => Piwik::translate('Actions_ColumnClicksDocumentation'), + ); + } + + public function configureView(ViewDataTable $view) + { + $view->config->columns_to_display = array('label', 'nb_visits', 'nb_hits'); + $view->config->show_exclude_low_population = false; + + $this->addBaseDisplayProperties($view); + } +} diff --git a/plugins/Actions/Reports/GetEntryPageTitles.php b/plugins/Actions/Reports/GetEntryPageTitles.php new file mode 100644 index 0000000000000000000000000000000000000000..6dcb5593d970cffa67f74c2b90906bc8cc60cf72 --- /dev/null +++ b/plugins/Actions/Reports/GetEntryPageTitles.php @@ -0,0 +1,95 @@ +dimension = new EntryPageTitle(); + $this->name = Piwik::translate('Actions_EntryPageTitles'); + $this->documentation = Piwik::translate('Actions_EntryPageTitlesReportDocumentation', '
') + . ' ' . Piwik::translate('General_UsePlusMinusIconsDocumentation'); + $this->metrics = array('entry_nb_visits', 'entry_bounce_count'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + $this->order = 6; + $this->actionToLoadSubTables = $this->action; + $this->subcategoryId = 'Actions_SubmenuPagesEntry'; + $this->hasGoalMetrics = true; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + $widgetsList->addWidgetConfig($factory->createWidget()->setName('Actions_WidgetEntryPageTitles')); + } + + public function getProcessedMetrics() + { + $result = parent::getProcessedMetrics(); + + // these metrics are not displayed in the API.getProcessedReport version of this report, + // so they are removed here. + unset($result['avg_time_on_page']); + unset($result['exit_rate']); + + return $result; + } + + protected function getMetricsDocumentation() + { + $metrics = parent::getMetricsDocumentation(); + $metrics['bounce_rate'] = Piwik::translate('General_ColumnPageBounceRateDocumentation'); + + // remove these metrics from API.getProcessedReport version of this report + unset($metrics['avg_time_on_page']); + unset($metrics['exit_rate']); + + return $metrics; + } + + public function configureView(ViewDataTable $view) + { + $view->config->columns_to_display = array('label', 'entry_nb_visits', 'entry_bounce_count', 'bounce_rate'); + $view->config->title = $this->name; + + $view->requestConfig->filter_sort_column = 'entry_nb_visits'; + + $this->addPageDisplayProperties($view); + $this->addBaseDisplayProperties($view); + + $view->config->show_goals = true; + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Actions', 'getPageTitles'), + ReportsProvider::factory('Actions', 'getEntryPageUrls'), + ); + } +} diff --git a/plugins/Actions/Reports/GetEntryPageUrls.php b/plugins/Actions/Reports/GetEntryPageUrls.php new file mode 100644 index 0000000000000000000000000000000000000000..aa1e222e75ce01077fb5a2c6bcce8b6659339833 --- /dev/null +++ b/plugins/Actions/Reports/GetEntryPageUrls.php @@ -0,0 +1,88 @@ +dimension = new EntryPageUrl(); + $this->name = Piwik::translate('Actions_SubmenuPagesEntry'); + $this->documentation = Piwik::translate('Actions_EntryPagesReportDocumentation', '
') + . '
' . Piwik::translate('General_UsePlusMinusIconsDocumentation'); + + $this->metrics = array('entry_nb_visits', 'entry_bounce_count'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + $this->order = 3; + + $this->actionToLoadSubTables = $this->action; + $this->subcategoryId = 'Actions_SubmenuPagesEntry'; + $this->hasGoalMetrics = true; + } + + public function getProcessedMetrics() + { + $result = parent::getProcessedMetrics(); + + // these metrics are not displayed in the API.getProcessedReport version of this report, + // so they are removed here. + unset($result['avg_time_on_page']); + unset($result['exit_rate']); + + return $result; + } + + protected function getMetricsDocumentation() + { + $metrics = parent::getMetricsDocumentation(); + $metrics['bounce_rate'] = Piwik::translate('General_ColumnPageBounceRateDocumentation'); + + unset($metrics['avg_time_on_page']); + unset($metrics['exit_rate']); + + return $metrics; + } + + public function configureView(ViewDataTable $view) + { + $view->config->title = $this->name; + $view->config->columns_to_display = array('label', 'entry_nb_visits', 'entry_bounce_count', 'bounce_rate'); + $view->requestConfig->filter_sort_column = 'entry_nb_visits'; + $view->requestConfig->filter_sort_order = 'desc'; + + $this->addPageDisplayProperties($view); + $this->addBaseDisplayProperties($view); + + $view->config->show_goals = true; + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Actions', 'getEntryPageTitles'), + ); + } +} diff --git a/plugins/Actions/Reports/GetExitPageTitles.php b/plugins/Actions/Reports/GetExitPageTitles.php new file mode 100644 index 0000000000000000000000000000000000000000..addfd426146a3a6557528f7f5f6fa39717fe5dfe --- /dev/null +++ b/plugins/Actions/Reports/GetExitPageTitles.php @@ -0,0 +1,101 @@ +dimension = new ExitPageTitle(); + $this->name = Piwik::translate('Actions_ExitPageTitles'); + $this->documentation = Piwik::translate('Actions_ExitPageTitlesReportDocumentation', '
') + . ' ' . Piwik::translate('General_UsePlusMinusIconsDocumentation'); + $this->subcategoryId = 'Actions_SubmenuPagesExit'; + + $this->metrics = array('exit_nb_visits', 'nb_visits'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + $this->order = 7; + + $this->actionToLoadSubTables = $this->action; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + // we have to do it manually since it's only done automatically if a subcategoryId is specified, + // we do not set a subcategoryId since this report is not supposed to be shown in the UI + $widgetsList->addWidgetConfig($factory->createWidget()); + } + + public function getProcessedMetrics() + { + $result = parent::getProcessedMetrics(); + + // these metrics are not displayed in the API.getProcessedReport version of this report, + // so they are removed here. + unset($result['bounce_rate']); + unset($result['avg_time_on_page']); + + return $result; + } + + public function getMetrics() + { + $metrics = parent::getMetrics(); + $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviews'); + + return $metrics; + } + + protected function getMetricsDocumentation() + { + $metrics = parent::getMetricsDocumentation(); + $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviewsDocumentation'); + + unset($metrics['bounce_rate']); + unset($metrics['avg_time_on_page']); + + return $metrics; + } + + public function configureView(ViewDataTable $view) + { + $view->config->title = $this->name; + $view->config->columns_to_display = array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate'); + + $this->addPageDisplayProperties($view); + $this->addBaseDisplayProperties($view); + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Actions', 'getPageTitles'), + ReportsProvider::factory('Actions', 'getExitPageUrls'), + ); + } +} diff --git a/plugins/Actions/Reports/GetExitPageUrls.php b/plugins/Actions/Reports/GetExitPageUrls.php new file mode 100644 index 0000000000000000000000000000000000000000..095395a6237a926b3b41b94e85572557fb64d7ba --- /dev/null +++ b/plugins/Actions/Reports/GetExitPageUrls.php @@ -0,0 +1,101 @@ +dimension = new ExitPageUrl(); + $this->name = Piwik::translate('Actions_SubmenuPagesExit'); + $this->documentation = Piwik::translate('Actions_ExitPagesReportDocumentation', '
') + . '
' . Piwik::translate('General_UsePlusMinusIconsDocumentation'); + + $this->metrics = array('exit_nb_visits', 'nb_visits'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + $this->actionToLoadSubTables = $this->action; + + $this->order = 4; + + $this->subcategoryId = 'Actions_SubmenuPagesExit'; + } + + public function getProcessedMetrics() + { + $result = parent::getProcessedMetrics(); + + // these metrics are not displayed in the API.getProcessedReport version of this report, + // so they are removed here. + unset($result['bounce_rate']); + unset($result['avg_time_on_page']); + + return $result; + } + + public function getMetrics() + { + $metrics = parent::getMetrics(); + $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviews'); + + unset($metrics['bounce_rate']); + unset($metrics['avg_time_on_page']); + + return $metrics; + } + + protected function getMetricsDocumentation() + { + $metrics = parent::getMetricsDocumentation(); + $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviewsDocumentation'); + + return $metrics; + } + + public function configureView(ViewDataTable $view) + { + $view->config->self_url = Request::getCurrentUrlWithoutGenericFilters(array( + 'module' => 'Actions', + 'action' => 'getExitPageUrls', + )); + + $view->config->title = $this->name; + + $view->config->columns_to_display = array('label', 'exit_nb_visits', 'nb_visits', 'exit_rate'); + $view->requestConfig->filter_sort_column = 'exit_nb_visits'; + $view->requestConfig->filter_sort_order = 'desc'; + + $this->addPageDisplayProperties($view); + $this->addBaseDisplayProperties($view); + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Actions', 'getExitPageTitles'), + ); + } +} diff --git a/plugins/Actions/Reports/GetOutlinks.php b/plugins/Actions/Reports/GetOutlinks.php new file mode 100644 index 0000000000000000000000000000000000000000..9b2cda81d3d1ba80f02b5d40e4d1c2bfb3a46435 --- /dev/null +++ b/plugins/Actions/Reports/GetOutlinks.php @@ -0,0 +1,59 @@ +dimension = new ClickedUrl(); + $this->name = Piwik::translate('General_Outlinks'); + $this->documentation = Piwik::translate('Actions_OutlinksReportDocumentation') . ' ' + . Piwik::translate('Actions_OutlinkDocumentation') . '
' + . Piwik::translate('General_UsePlusMinusIconsDocumentation'); + + $this->metrics = array('nb_visits', 'nb_hits'); + $this->order = 8; + + $this->actionToLoadSubTables = $this->action; + + $this->subcategoryId = 'General_Outlinks'; + } + + public function getMetrics() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicks'), + 'nb_hits' => Piwik::translate('Actions_ColumnClicks'), + ); + } + + protected function getMetricsDocumentation() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnUniqueClicksDocumentation'), + 'nb_hits' => Piwik::translate('Actions_ColumnClicksDocumentation'), + ); + } + + public function configureView(ViewDataTable $view) + { + $view->config->columns_to_display = array('label', 'nb_visits', 'nb_hits'); + $view->config->show_exclude_low_population = false; + + $this->addBaseDisplayProperties($view); + } +} diff --git a/plugins/Actions/Reports/GetPageTitles.php b/plugins/Actions/Reports/GetPageTitles.php new file mode 100644 index 0000000000000000000000000000000000000000..d08505d598ad6888fca9fd2499918d04d24e6fcf --- /dev/null +++ b/plugins/Actions/Reports/GetPageTitles.php @@ -0,0 +1,96 @@ +dimension = new PageTitle(); + $this->name = Piwik::translate('Actions_SubmenuPageTitles'); + $this->documentation = Piwik::translate( + 'Actions_PageTitlesReportDocumentation', + ['
', htmlentities('', ENT_COMPAT | ENT_HTML401, 'UTF-8')] + ); + + $this->order = 5; + $this->metrics = array('nb_hits', 'nb_visits'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + + $this->actionToLoadSubTables = $this->action; + $this->subcategoryId = 'Actions_SubmenuPageTitles'; + $this->hasGoalMetrics = true; + } + + public function getMetrics() + { + $metrics = parent::getMetrics(); + $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviews'); + + return $metrics; + } + + protected function getMetricsDocumentation() + { + $metrics = parent::getMetricsDocumentation(); + $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviewsDocumentation'); + $metrics['bounce_rate'] = Piwik::translate('General_ColumnPageBounceRateDocumentation'); + + return $metrics; + } + + public function configureView(ViewDataTable $view) + { + $view->config->self_url = Request::getCurrentUrlWithoutGenericFilters(array( + 'module' => $this->module, + 'action' => 'getPageTitles', + )); + + $view->config->title = $this->name; + + $view->config->columns_to_display = array('label', 'nb_hits', 'nb_visits', 'bounce_rate', + 'avg_time_on_page', 'exit_rate'); + + if (version_compare(DbHelper::getInstallVersion(), '4.0.0-b1', '<')) { + $view->config->columns_to_display[] = 'avg_time_generation'; + } + + $this->addPageDisplayProperties($view); + $this->addBaseDisplayProperties($view); + + $view->config->show_goals = true; + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Actions', 'getEntryPageTitles'), + ReportsProvider::factory('Actions', 'getExitPageTitles'), + ); + } +} diff --git a/plugins/Actions/Reports/GetPageTitlesFollowingSiteSearch.php b/plugins/Actions/Reports/GetPageTitlesFollowingSiteSearch.php new file mode 100644 index 0000000000000000000000000000000000000000..59c344cb31860e4b0af340d887315964f2646d30 --- /dev/null +++ b/plugins/Actions/Reports/GetPageTitlesFollowingSiteSearch.php @@ -0,0 +1,87 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Actions\Columns\DestinationPage; +use Piwik\Plugins\Actions\Columns\Metrics\AveragePageGenerationTime; +use Piwik\Plugins\Actions\Columns\Metrics\AverageTimeOnPage; +use Piwik\Plugins\Actions\Columns\Metrics\BounceRate; +use Piwik\Plugins\Actions\Columns\Metrics\ExitRate; +use Piwik\Plugin\ReportsProvider; + +class GetPageTitlesFollowingSiteSearch extends SiteSearchBase +{ + protected function init() + { + parent::init(); + $this->dimension = new DestinationPage(); + $this->name = Piwik::translate('Actions_WidgetPageTitlesFollowingSearch'); + $this->documentation = Piwik::translate('Actions_SiteSearchFollowingPagesDoc') . '<br/>' . Piwik::translate('General_UsePlusMinusIconsDocumentation'); + $this->metrics = array('nb_hits_following_search', 'nb_hits'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + $this->order = 19; + + $this->subcategoryId = 'Actions_SubmenuSitesearch'; + } + + public function configureView(ViewDataTable $view) + { + $title = Piwik::translate('Actions_WidgetPageUrlsFollowingSearch'); + + $this->configureViewForUrlAndTitle($view, $title); + } + + public function getMetrics() + { + return array( + 'nb_hits_following_search' => Piwik::translate('General_ColumnViewedAfterSearch'), + 'nb_hits' => Piwik::translate('General_ColumnPageviews'), + ); + } + + public function getProcessedMetrics() + { + return array(); + } + + protected function getMetricsDocumentation() + { + return array( + 'nb_hits_following_search' => Piwik::translate('General_ColumnViewedAfterSearchDocumentation'), + 'nb_hits' => Piwik::translate('General_ColumnPageviewsDocumentation'), + ); + } + + protected function configureViewForUrlAndTitle(ViewDataTable $view, $title) + { + $view->config->title = $title; + $view->config->columns_to_display = array('label', 'nb_hits_following_search', 'nb_hits'); + $view->config->show_exclude_low_population = false; + $view->requestConfig->filter_sort_column = 'nb_hits_following_search'; + $view->requestConfig->filter_sort_order = 'desc'; + + $this->addExcludeLowPopDisplayProperties($view); + $this->addBaseDisplayProperties($view); + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Actions', 'getPageUrlsFollowingSiteSearch'), + ); + } +} diff --git a/plugins/Actions/Reports/GetPageUrls.php b/plugins/Actions/Reports/GetPageUrls.php new file mode 100644 index 0000000000000000000000000000000000000000..a00af4f077a377fb7680e252b75449e3e261be1a --- /dev/null +++ b/plugins/Actions/Reports/GetPageUrls.php @@ -0,0 +1,98 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\Reports; + +use Piwik\DbHelper; +use Piwik\Piwik; +use Piwik\Plugin\ReportsProvider; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Actions\Columns\Metrics\AveragePageGenerationTime; +use Piwik\Plugins\Actions\Columns\Metrics\BounceRate; +use Piwik\Plugins\Actions\Columns\PageUrl; +use Piwik\Plugins\Actions\Columns\Metrics\ExitRate; +use Piwik\Plugins\Actions\Columns\Metrics\AverageTimeOnPage; +use Piwik\Report\ReportWidgetFactory; +use Piwik\Widget\WidgetsList; + +class GetPageUrls extends Base +{ + protected function init() + { + parent::init(); + + $this->dimension = new PageUrl(); + $this->name = Piwik::translate('Actions_PageUrls'); + $this->documentation = Piwik::translate('Actions_PagesReportDocumentation', '<br />') + . '<br />' . Piwik::translate('General_UsePlusMinusIconsDocumentation'); + + $this->actionToLoadSubTables = $this->action; + $this->order = 2; + $this->metrics = array('nb_hits', 'nb_visits'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + + $this->subcategoryId = 'General_Pages'; + $this->hasGoalMetrics = true; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + $widgetsList->addWidgetConfig($factory->createWidget()->setName($this->subcategoryId)); + } + + public function getMetrics() + { + $metrics = parent::getMetrics(); + $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviews'); + + return $metrics; + } + + protected function getMetricsDocumentation() + { + $metrics = parent::getMetricsDocumentation(); + $metrics['nb_visits'] = Piwik::translate('General_ColumnUniquePageviewsDocumentation'); + $metrics['bounce_rate'] = Piwik::translate('General_ColumnPageBounceRateDocumentation'); + + return $metrics; + } + + public function configureView(ViewDataTable $view) + { + $view->config->columns_to_display = array('label', 'nb_hits', 'nb_visits', 'bounce_rate', + 'avg_time_on_page', 'exit_rate'); + + if (version_compare(DbHelper::getInstallVersion(), '4.0.0-b1', '<')) { + $view->config->columns_to_display[] = 'avg_time_generation'; + } + + $this->addPageDisplayProperties($view); + $this->addBaseDisplayProperties($view); + + $view->config->show_goals = true; + + // related reports are only shown on performance page + if ($view->requestConfig->getRequestParam('performance') !== '1') { + $view->config->related_reports = []; + } + } + + public function getRelatedReports() + { + return [ + ReportsProvider::factory('Actions', 'getEntryPageUrls'), + ReportsProvider::factory('Actions', 'getExitPageUrls'), + ]; + } +} diff --git a/plugins/Actions/Reports/GetPageUrlsFollowingSiteSearch.php b/plugins/Actions/Reports/GetPageUrlsFollowingSiteSearch.php new file mode 100644 index 0000000000000000000000000000000000000000..b661e6f650352b968322618ac475e6836e4860dc --- /dev/null +++ b/plugins/Actions/Reports/GetPageUrlsFollowingSiteSearch.php @@ -0,0 +1,43 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Actions\Columns\DestinationPage; +use Piwik\Plugin\ReportsProvider; + +class GetPageUrlsFollowingSiteSearch extends GetPageTitlesFollowingSiteSearch +{ + protected function init() + { + parent::init(); + $this->dimension = new DestinationPage(); + $this->name = Piwik::translate('Actions_WidgetPageUrlsFollowingSearch'); + $this->documentation = Piwik::translate('Actions_SiteSearchFollowingPagesDoc') . '<br/>' . Piwik::translate('General_UsePlusMinusIconsDocumentation'); + $this->order = 16; + + $this->subcategoryId = 'Actions_SubmenuSitesearch'; + } + + public function configureView(ViewDataTable $view) + { + $title = Piwik::translate('Actions_WidgetPageTitlesFollowingSearch'); + + $this->configureViewForUrlAndTitle($view, $title); + } + + public function getRelatedReports() + { + return array( + ReportsProvider::factory('Actions', 'getPageTitlesFollowingSiteSearch'), + ); + } +} diff --git a/plugins/Actions/Reports/GetSiteSearchCategories.php b/plugins/Actions/Reports/GetSiteSearchCategories.php new file mode 100644 index 0000000000000000000000000000000000000000..cbed4c1a936a3678ed1639e6803c175f4ec07e8c --- /dev/null +++ b/plugins/Actions/Reports/GetSiteSearchCategories.php @@ -0,0 +1,64 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Actions\Columns\SearchCategory; +use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable; + +class GetSiteSearchCategories extends SiteSearchBase +{ + protected function init() + { + parent::init(); + $this->dimension = new SearchCategory(); + $this->name = Piwik::translate('Actions_WidgetSearchCategories'); + $this->documentation = Piwik::translate('Actions_SiteSearchCategories1') . '<br/>' . Piwik::translate('Actions_SiteSearchCategories2'); + $this->metrics = array('nb_visits', 'nb_pages_per_search', 'exit_rate'); + $this->order = 20; + + $this->subcategoryId = 'Actions_SubmenuSitesearch'; + } + + protected function isEnabledForIdSites($idSites, $idSite) + { + return parent::isEnabledForIdSites($idSites, $idSite); + } + + public function getMetrics() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnSearches'), + 'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearch'), + 'exit_rate' => Piwik::translate('Actions_ColumnSearchExits'), + ); + } + + protected function getMetricsDocumentation() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnSearchesDocumentation'), + 'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearchDocumentation'), + 'exit_rate' => Piwik::translate('Actions_ColumnSearchExitsDocumentation'), + ); + } + + public function configureView(ViewDataTable $view) + { + $view->config->columns_to_display = array('label', 'nb_visits', 'nb_pages_per_search'); + $view->config->show_table_all_columns = false; + $view->config->show_bar_chart = false; + + if ($view->isViewDataTableId(HtmlTable::ID)) { + $view->config->disable_row_evolution = false; + } + } +} diff --git a/plugins/Actions/Reports/GetSiteSearchKeywords.php b/plugins/Actions/Reports/GetSiteSearchKeywords.php new file mode 100644 index 0000000000000000000000000000000000000000..bd376c7a8b9ba03c25b1fa9ad7e1af204c6bf8a3 --- /dev/null +++ b/plugins/Actions/Reports/GetSiteSearchKeywords.php @@ -0,0 +1,69 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Actions\Columns\Keyword; +use Piwik\Plugins\Actions\Columns\Metrics\AveragePageGenerationTime; +use Piwik\Plugins\Actions\Columns\Metrics\AverageTimeOnPage; +use Piwik\Plugins\Actions\Columns\Metrics\BounceRate; +use Piwik\Plugins\Actions\Columns\Metrics\ExitRate; + +class GetSiteSearchKeywords extends SiteSearchBase +{ + protected function init() + { + parent::init(); + $this->dimension = new Keyword(); + $this->name = Piwik::translate('Actions_WidgetSearchKeywords'); + $this->documentation = Piwik::translate('Actions_SiteSearchKeywordsDocumentation') . '<br/><br/>' . Piwik::translate('Actions_SiteSearchIntro'); + $this->metrics = array('nb_visits', 'nb_pages_per_search'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + $this->order = 15; + $this->subcategoryId = 'Actions_SubmenuSitesearch'; + } + + public function getMetrics() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnSearches'), + 'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearch'), + ); + } + + public function getProcessedMetrics() + { + return array( + 'exit_rate' => Piwik::translate('Actions_ColumnSearchExits'), + ); + } + + protected function getMetricsDocumentation() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnSearchesDocumentation'), + 'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearchDocumentation'), + 'exit_rate' => Piwik::translate('Actions_ColumnSearchExitsDocumentation'), + ); + } + + public function configureView(ViewDataTable $view) + { + $view->config->columns_to_display = array('label', 'nb_visits', 'nb_pages_per_search', 'exit_rate'); + + $this->addSiteSearchDisplayProperties($view); + } +} diff --git a/plugins/Actions/Reports/GetSiteSearchNoResultKeywords.php b/plugins/Actions/Reports/GetSiteSearchNoResultKeywords.php new file mode 100644 index 0000000000000000000000000000000000000000..5bdc192e2fc9ee8f31bcfea5603997ca586de6bf --- /dev/null +++ b/plugins/Actions/Reports/GetSiteSearchNoResultKeywords.php @@ -0,0 +1,68 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Actions\Columns\KeywordwithNoSearchResult; +use Piwik\Plugins\Actions\Columns\Metrics\AveragePageGenerationTime; +use Piwik\Plugins\Actions\Columns\Metrics\AverageTimeOnPage; +use Piwik\Plugins\Actions\Columns\Metrics\BounceRate; +use Piwik\Plugins\Actions\Columns\Metrics\ExitRate; + +class GetSiteSearchNoResultKeywords extends SiteSearchBase +{ + protected function init() + { + parent::init(); + $this->dimension = new KeywordwithNoSearchResult(); + $this->name = Piwik::translate('Actions_WidgetSearchNoResultKeywords'); + $this->documentation = Piwik::translate('Actions_SiteSearchIntro') . '<br /><br />' . Piwik::translate('Actions_SiteSearchKeywordsNoResultDocumentation'); + $this->metrics = array('nb_visits'); + $this->processedMetrics = array( + new AverageTimeOnPage(), + new BounceRate(), + new ExitRate(), + new AveragePageGenerationTime(), + ); + $this->order = 18; + + $this->subcategoryId = 'Actions_SubmenuSitesearch'; + } + + public function getMetrics() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnSearches'), + ); + } + + public function getProcessedMetrics() + { + return array( + 'exit_rate' => Piwik::translate('Actions_ColumnSearchExits'), + ); + } + + protected function getMetricsDocumentation() + { + return array( + 'nb_visits' => Piwik::translate('Actions_ColumnSearchesDocumentation'), + 'exit_rate' => Piwik::translate('Actions_ColumnSearchExitsDocumentation'), + ); + } + + public function configureView(ViewDataTable $view) + { + $view->config->columns_to_display = array('label', 'nb_visits', 'exit_rate'); + + $this->addSiteSearchDisplayProperties($view); + } +} diff --git a/plugins/Actions/Reports/SiteSearchBase.php b/plugins/Actions/Reports/SiteSearchBase.php new file mode 100644 index 0000000000000000000000000000000000000000..f69baaea2760f5f5bfcab3276afbb4b331096fac --- /dev/null +++ b/plugins/Actions/Reports/SiteSearchBase.php @@ -0,0 +1,67 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\Reports; + +use Piwik\Common; +use Piwik\Piwik; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Actions\Actions; +use Piwik\Url; + +abstract class SiteSearchBase extends Base +{ + protected function init() + { + parent::init(); + $this->categoryId = 'General_Actions'; + $this->onlineGuideUrl = Url::addCampaignParametersToMatomoLink('https://matomo.org/docs/site-search/'); + } + + public function isEnabled() + { + $idSites = Common::getRequestVar('idSites', '', 'string'); + $idSite = Common::getRequestVar('idSite', 0, 'int'); + + return $this->isEnabledForIdSites($idSites, $idSite); + } + + protected function isEnabledForIdSites($idSites, $idSite) + { + $actions = new Actions(); + return $actions->isSiteSearchEnabled($idSites, $idSite); + } + + public function configureReportMetadata(&$availableReports, $infos) + { + $idSite = array($infos['idSite']); + + if (!$this->isEnabledForIdSites($idSite, Common::getRequestVar('idSite', 0, 'int'))) { + return; + } + + $report = $this->buildReportMetadata(); + + if (!empty($report)) { + $availableReports[] = $report; + } + } + + protected function addSiteSearchDisplayProperties(ViewDataTable $view) + { + $view->config->addTranslations(array( + 'nb_visits' => Piwik::translate('Actions_ColumnSearches'), + 'exit_rate' => str_replace("% ", "% ", Piwik::translate('Actions_ColumnSearchExits')), + 'nb_pages_per_search' => Piwik::translate('Actions_ColumnPagesPerSearch'), + )); + + $view->config->show_bar_chart = false; + $view->config->show_table_all_columns = false; + } +} diff --git a/plugins/Actions/Segment.php b/plugins/Actions/Segment.php new file mode 100644 index 0000000000000000000000000000000000000000..921613d57e600553cd7b8e291d71eead2c66212a --- /dev/null +++ b/plugins/Actions/Segment.php @@ -0,0 +1,22 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions; + +/** + * Actions segment base class + */ +class Segment extends \Piwik\Plugin\Segment +{ + protected function init() + { + $this->setCategory('General_Actions'); + $this->setSqlFilter('\\Piwik\\Tracker\\TableLogAction::getIdActionFromSegment'); + } +} diff --git a/plugins/Actions/Tracker/ActionsRequestProcessor.php b/plugins/Actions/Tracker/ActionsRequestProcessor.php new file mode 100644 index 0000000000000000000000000000000000000000..0f7ed748c2b1cb43e21407e502d9d498939dff0d --- /dev/null +++ b/plugins/Actions/Tracker/ActionsRequestProcessor.php @@ -0,0 +1,106 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\Tracker; + +use Piwik\Tracker\Action; +use Piwik\Tracker\Request; +use Piwik\Tracker\RequestProcessor; +use Piwik\Tracker\Visit\VisitProperties; +use Piwik\Tracker\Visitor; + +/** + * Handles actions detection and recording during tracker requests. + * + * ## Request Metadata + * + * This RequestProcessor exposes the following metadata for the **Actions** plugin: + * + * **action**: Contains the `Action` instance that represents the action being tracked for + * the current tracking request. + * + * Set in `processRequestParams()`. + * + * Other RequestProcessors can unset this value to skip actions recording or + * change the value to change how they are recorded. + * + * **idReferrerActionUrl**: The idaction of the URL action that is the referrer for the action + * being tracked. + * + * Set in `processRequestParams()`. + * + * Can be changed/unset to change the current action's referrer action. + * + * **idReferrerActionName**: The idaction of the name action that is the referrer for the action + * being tracked. + * + * Set in `processRequestParams()`. + * + * Can be changed/unset to change the current action's referrer action. + */ +class ActionsRequestProcessor extends RequestProcessor +{ + public function processRequestParams(VisitProperties $visitProperties, Request $request) + { + // normal page view, potentially triggering a URL matching goal + $action = Action::factory($request); + $action->writeDebugInfo(); + + $request->setMetadata('Actions', 'action', $action); + + // save the exit actions of the last action in this visit as the referrer actions for the action being tracked. + // when the visit is updated, these columns will be changed, so we have to do this before recordLogs + $request->setMetadata( + 'Actions', + 'idReferrerActionUrl', + $visitProperties->getProperty('visit_exit_idaction_url') + ); + $request->setMetadata( + 'Actions', + 'idReferrerActionName', + $visitProperties->getProperty('visit_exit_idaction_name') + ); + + return false; + } + + public function afterRequestProcessed(VisitProperties $visitProperties, Request $request) + { + /** @var Action $action */ + $action = $request->getMetadata('Actions', 'action'); + + if (!empty($action)) { // other plugins can unset the action if they want + $action->loadIdsFromLogActionTable(); + } + + return false; + } + + public function recordLogs(VisitProperties $visitProperties, Request $request) + { + /** @var Action $action */ + $action = $request->getMetadata('Actions', 'action'); + + if ( + $action !== null + && !$request->getMetadata('CoreHome', 'visitorNotFoundInDb') + ) { + $idReferrerActionUrl = 0; + $idReferrerActionName = 0; + + if (!$request->getMetadata('CoreHome', 'isNewVisit')) { + $idReferrerActionUrl = $request->getMetadata('Actions', 'idReferrerActionUrl'); + $idReferrerActionName = $request->getMetadata('Actions', 'idReferrerActionName'); + } + + $visitor = Visitor::makeFromVisitProperties($visitProperties, $request); + $action->record($visitor, $idReferrerActionUrl, $idReferrerActionName); + } + } +} diff --git a/plugins/Actions/VisitorDetails.php b/plugins/Actions/VisitorDetails.php new file mode 100644 index 0000000000000000000000000000000000000000..6ee1ee922127cc0f68b878b89d4f393c4edd08d3 --- /dev/null +++ b/plugins/Actions/VisitorDetails.php @@ -0,0 +1,523 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions; + +use Piwik\Common; +use Piwik\Date; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin; +use Piwik\Plugins\Live\VisitorDetailsAbstract; +use Piwik\Site; +use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; + +class VisitorDetails extends VisitorDetailsAbstract +{ + public function extendVisitorDetails(&$visitor) + { + $visitor['searches'] = $this->details['visit_total_searches']; + $visitor['actions'] = $this->details['visit_total_actions']; + $visitor['interactions'] = $this->details['visit_total_interactions']; + } + + public function provideActionsForVisitIds(&$actions, $visitIds) + { + $actionDetails = $this->queryActionsForVisits($visitIds); + // use while / array_shift combination instead of foreach to save memory + while (is_array($actionDetails) && count($actionDetails)) { + $action = array_shift($actionDetails); + $idVisit = $action['idvisit']; + unset($action['idvisit']); + $actions[$idVisit][] = $action; + } + } + + + public function provideActionsForVisit(&$actions, $visitorDetails) + { + $actionDetails = $actions; + + $formatter = new Formatter(); + + // Enrich with time spent per action + $nextActionId = 0; + foreach ($actionDetails as $idx => &$action) { + if ($idx < $nextActionId || !$this->isPageView($action)) { + unset($action['timeSpentRef']); + continue; // skip to next page view + } + + $action['timeSpent'] = 0; + + // search for next action with timeSpentRef + $nextActionId = $idx; + $nextAction = null; + + do { + $nextActionId++; + + $nextAction = isset($actionDetails[$nextActionId]) ? $actionDetails[$nextActionId] : null; + + if (is_null($nextAction)) { + // Last action of a visit. + // By default, Matomo does not know how long the user stayed on the page + // If enableHeartBeatTimer() is used in piwik.js then we can find the accurate time on page for the last pageview + $visitTotalTime = $visitorDetails['visitDuration']; + $timeOfLastAction = Date::factory($action['serverTimePretty'])->getTimestamp(); + + $timeSpentOnAllActionsApartFromLastOne = ($timeOfLastAction - $visitorDetails['firstActionTimestamp']); + $timeSpentOnPage = $visitTotalTime - $timeSpentOnAllActionsApartFromLastOne; + + // Safe net, we assume the time is correct when it's more than 10 seconds + if ($timeSpentOnPage > 10) { + $action['timeSpent'] = $timeSpentOnPage; + } + break; + } + + if (!array_key_exists('timeSpentRef', $nextAction)) { + continue; + } + + // Set the time spent for this action (which is the timeSpentRef of the next action) + if ($nextAction) { + $action['timeSpent'] += $nextAction['timeSpentRef'] ?? 0; + } + + // sum spent time until next page view + if ($this->isPageView($nextAction)) { + break; + } + } while (isset($actionDetails[$nextActionId])); + + if (isset($action['timeSpent'])) { + $action['timeSpentPretty'] = $formatter->getPrettyTimeFromSeconds($action['timeSpent'], true); + } + + unset($action['timeSpentRef']); + } + + $actions = $actionDetails; + } + + private function shouldHandleAction($action) + { + $actionTypesToHandle = array( + Action::TYPE_PAGE_URL, + Action::TYPE_PAGE_TITLE, + Action::TYPE_SITE_SEARCH, + Action::TYPE_EVENT, + Action::TYPE_OUTLINK, + Action::TYPE_DOWNLOAD, + ); + + if (empty($action['type'])) { + return !empty($action['eventType']); + } + + return in_array($action['type'], $actionTypesToHandle) || !empty($action['eventType']); + } + + private function isPageView($action) + { + if (empty($action['type'])) { + return false; + } + + $pageViewTypes = array( + Action::TYPE_PAGE_URL, + Action::TYPE_PAGE_TITLE, + ); + + return in_array($action['type'], $pageViewTypes); + } + + public function extendActionDetails(&$action, $nextAction, $visitorDetails) + { + $actionType = ''; + if (!empty($action['type'])) { + $actionType = $action['type']; + } + + $formatter = new Formatter(); + + if ($actionType == Action::TYPE_SITE_SEARCH) { + // Handle Site Search + $action['siteSearchKeyword'] = $action['pageTitle']; + $action['siteSearchCategory'] = $action['search_cat']; + $action['siteSearchCount'] = $action['search_count']; + unset($action['pageTitle']); + } + + unset($action['search_cat']); + unset($action['search_count']); + + // Generation time + if ($this->shouldHandleAction($action) && empty($action['eventType']) && isset($action['custom_float']) && $action['custom_float'] > 0) { + $action['generationTimeMilliseconds'] = $action['custom_float']; + $action['generationTime'] = $formatter->getPrettyTimeFromSeconds($action['custom_float'] / 1000, true); + unset($action['custom_float']); + } + + if (array_key_exists('custom_float', $action) && is_null($action['custom_float'])) { + unset($action['custom_float']); + } + + if (!array_key_exists('pageLoadTime', $action) || $action['pageLoadTime'] <= 0) { + unset($action['pageLoadTime']); + } else { + $action['pageLoadTimeMilliseconds'] = $action['pageLoadTime']; + $action['pageLoadTime'] = $formatter->getPrettyTimeFromSeconds($action['pageLoadTime'] / 1000, true); + } + + if (array_key_exists('pageview_position', $action)) { + $action['pageviewPosition'] = $action['pageview_position']; + unset($action['pageview_position']); + } + + // Reconstruct url from prefix + if (array_key_exists('url', $action) && array_key_exists('url_prefix', $action)) { + if ($action['url'] && stripos($action['url'], 'http://') !== 0 && stripos($action['url'], 'https://') !== 0) { + $url = PageUrl::reconstructNormalizedUrl($action['url'], $action['url_prefix']); + $url = Common::unsanitizeInputValue($url); + $action['url'] = $url; + } + + unset($action['url_prefix']); + } + + if (!empty($action['url']) && strpos($action['url'], 'http://') === 0) { + $host = parse_url($action['url'], PHP_URL_HOST); + + if ($host && PageUrl::shouldUseHttpsHost($visitorDetails['idSite'], $host)) { + $action['url'] = 'https://' . mb_substr($action['url'], 7 /* = strlen('http://') */); + } + } + + switch ($actionType) { + case 'goal': + $action['icon'] = 'plugins/Morpheus/images/goal.png'; + $action['iconSVG'] = 'plugins/Morpheus/images/goal.svg'; + $action['title'] = Piwik::translate('Goals_GoalConversion'); + $action['subtitle'] = $action['goalName']; + if (!empty($action['revenue'])) { + $action['subtitle'] .= ' (' . Piwik::translate('Goals_NRevenue', $formatter->getPrettyMoney($action['revenue'], $visitorDetails['idSite'])) . ')'; + } + break; + case Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER: + case Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART: + $action['icon'] = 'plugins/Morpheus/images/' . $actionType . '.png'; + $action['iconSVG'] = 'plugins/Morpheus/images/' . $actionType . '.svg'; + if ($actionType == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER) { + $action['title'] = Piwik::translate('CoreHome_VisitStatusOrdered') . ' (' . $action['orderId'] . ')'; + } else { + $action['title'] = Piwik::translate('Goals_AbandonedCart'); + } + + $itemNames = implode(', ', array_column($action['itemDetails'], 'itemName')); + $action['subtitle'] = Piwik::translate('Goals_NRevenue', $formatter->getPrettyMoney($action['revenue'], $visitorDetails['idSite'])); + $action['subtitle'] .= ' - ' . Piwik::translate('Goals_NItems', $action['items']) . ': ' . $itemNames . ')'; + break; + case Action::TYPE_CONTENT: + if (!empty($action['contentInteraction'])) { + $action['icon'] = 'plugins/Morpheus/images/contentinteraction.png'; + $action['iconSVG'] = 'plugins/Morpheus/images/contentinteraction.svg'; + $action['title'] = Piwik::translate('Contents_ContentInteraction') . ' (' . $action['contentInteraction'] . ')'; + } else { + $action['icon'] = 'plugins/Morpheus/images/contentimpression.png'; + $action['iconSVG'] = 'plugins/Morpheus/images/contentimpression.svg'; + $action['title'] = Piwik::translate('Contents_ContentImpression'); + } + + $action['subtitle'] = $action['contentName']; + if (!empty($action['contentPiece'])) { + $action['subtitle'] .= ' - ' . $action['contentPiece']; + } + break; + case Action::TYPE_DOWNLOAD: + $action['type'] = 'download'; + $action['icon'] = 'plugins/Morpheus/images/download.png'; + $action['iconSVG'] = 'plugins/Morpheus/images/download.svg'; + $action['title'] = Piwik::translate('General_Download'); + $action['subtitle'] = $action['url']; + break; + case Action::TYPE_OUTLINK: + $action['type'] = 'outlink'; + $action['icon'] = 'plugins/Morpheus/images/link.png'; + $action['iconSVG'] = 'plugins/Morpheus/images/link.svg'; + $action['title'] = Piwik::translate('General_Outlink'); + $action['subtitle'] = $action['url']; + break; + case Action::TYPE_SITE_SEARCH: + $action['type'] = 'search'; + $action['icon'] = 'plugins/Morpheus/images/search.png'; + $action['iconSVG'] = 'plugins/Morpheus/images/search.svg'; + $action['title'] = Piwik::translate('Actions_SubmenuSitesearch'); + $action['subtitle'] = $action['siteSearchKeyword']; + + if (!empty($action['siteSearchCategory'])) { + $action['subtitle'] .= ' - ' . Piwik::translate('Actions_ColumnSearchCategory') . ': ' . $action['siteSearchCategory']; + } + + if (!empty($action['siteSearchCount'])) { + $action['subtitle'] .= ' - ' . Piwik::translate('Actions_ColumnSearchResultsCount') . ': ' . $action['siteSearchCount']; + } + + break; + case Action::TYPE_PAGE_URL: + case Action::TYPE_PAGE_TITLE: + case '': + if (!isset($action['title'])) { + $action['title'] = $action['pageTitle']; + $action['subtitle'] = $action['url']; + } + $action['type'] = 'action'; + $action['icon'] = ''; + $action['iconSVG'] = 'plugins/Morpheus/images/action.svg'; + break; + } + + // Convert datetimes to the site timezone + $dateTimeVisit = Date::factory( + $action['serverTimePretty'], + Site::getTimezoneFor($visitorDetails['idSite']) + ); + $action['serverTimePretty'] = $dateTimeVisit->getLocalized(Date::DATETIME_FORMAT_SHORT); + $action['timestamp'] = $dateTimeVisit->getTimestamp(); + + unset($action['idlink_va']); + } + + /** + * @param array $idVisits + * @return array + */ + protected function queryActionsForVisits($idVisits) + { + $customFields = array(); + $customJoins = array(); + + Piwik::postEvent('Actions.getCustomActionDimensionFieldsAndJoins', array(&$customFields, &$customJoins)); + + $customFields = array_filter($customFields); + array_unshift($customFields, ''); // add empty element at first + $customActionDimensionFields = implode(', ', $customFields); + + // The second join is a LEFT join to allow returning records that don't have a matching page title + // eg. Downloads, Outlinks. For these, idaction_name is set to 0 + $pagePerformanceSelect = ''; + if (Plugin\Manager::getInstance()->isPluginActivated('PagePerformance')) { + $pagePerformanceSelect = '( log_link_visit_action.time_network + + log_link_visit_action.time_server + + log_link_visit_action.time_transfer + + log_link_visit_action.time_dom_completion + + log_link_visit_action.time_dom_processing + + log_link_visit_action.time_on_load ) AS pageLoadTime,'; + } + + $sql = " + SELECT + log_link_visit_action.idvisit, + COALESCE(log_action.type, log_action_title.type) AS type, + log_action.name AS url, + log_action.url_prefix, + log_action_title.name AS pageTitle, + log_action.idaction AS pageIdAction, + log_link_visit_action.idpageview, + log_link_visit_action.idlink_va, + log_link_visit_action.server_time as serverTimePretty, + log_link_visit_action.time_spent_ref_action as timeSpentRef, + log_link_visit_action.idlink_va AS pageId, + log_link_visit_action.custom_float, + $pagePerformanceSelect + log_link_visit_action.pageview_position, + log_link_visit_action.search_cat, + log_link_visit_action.search_count + " . $customActionDimensionFields . " + FROM `" . Common::prefixTable('log_link_visit_action') . "` AS log_link_visit_action + LEFT JOIN `" . Common::prefixTable('log_action') . "` AS log_action + ON log_link_visit_action.idaction_url = log_action.idaction + LEFT JOIN `" . Common::prefixTable('log_action') . "` AS log_action_title + ON log_link_visit_action.idaction_name = log_action_title.idaction + " . implode(" ", $customJoins) . " + WHERE log_link_visit_action.idvisit IN ('" . implode("','", $idVisits) . "') + ORDER BY log_link_visit_action.idvisit, log_link_visit_action.server_time, log_link_visit_action.idlink_va + "; + $actionDetails = $this->getDb()->fetchAll($sql); + return $actionDetails; + } + + + private $visitedPageUrls = array(); + private $siteSearchKeywords = array(); + private $pageGenerationTimeTotal = 0; + private $pageLoadTimeTotal = 0; + + public function initProfile($visits, &$profile) + { + $this->visitedPageUrls = array(); + $this->siteSearchKeywords = array(); + $this->pageGenerationTimeTotal = 0; + $this->pageLoadTimeTotal = 0; + $profile['totalActions'] = 0; + $profile['totalOutlinks'] = 0; + $profile['totalDownloads'] = 0; + $profile['totalSearches'] = 0; + $profile['totalPageViews'] = 0; + $profile['totalUniquePageViews'] = 0; + $profile['totalRevisitedPages'] = 0; + $profile['totalPageViewsWithTiming'] = 0; + $profile['totalPageViewsWithLoadTime'] = 0; + $profile['searches'] = array(); + } + + public function handleProfileVisit($visit, &$profile) + { + $profile['totalActions'] += $visit->getColumn('actions'); + } + + public function handleProfileAction($action, &$profile) + { + $this->handleIfDownloadAction($action, $profile); + $this->handleIfOutlinkAction($action, $profile); + $this->handleIfSiteSearchAction($action, $profile); + $this->handleIfPageViewAction($action, $profile); + $this->handleIfPageGenerationTime($action, $profile); + $this->handleIfPageLoadTime($action, $profile); + } + + public function finalizeProfile($visits, &$profile) + { + $profile['visitedPages'] = []; + + foreach ($this->visitedPageUrls as $visitedPageUrl => $count) { + $profile['visitedPages'][] = [ + 'url' => $visitedPageUrl, + 'count' => $count, + ]; + } + + usort($profile['visitedPages'], function ($a, $b) { + if ($a['count'] == $b['count']) { + return strcmp($a['url'], $b['url']); + } + + return $a['count'] > $b['count'] ? -1 : 1; + }); + + $this->handleSiteSearches($profile); + $this->handleAveragePageGenerationTime($profile); + $this->handleAveragePageLoadTime($profile); + } + + /** + * @param $action + */ + private function handleIfDownloadAction($action, &$profile) + { + if (empty($action['type']) || $action['type'] != 'download') { + return; + } + $profile['totalDownloads']++; + } + + /** + * @param $action + */ + private function handleIfOutlinkAction($action, &$profile) + { + if (empty($action['type']) || $action['type'] != 'outlink') { + return; + } + $profile['totalOutlinks']++; + } + + /** + * @param $action + */ + private function handleIfPageViewAction($action, &$profile) + { + if (empty($action['type']) || $action['type'] != 'action') { + return; + } + $profile['totalPageViews']++; + $pageUrl = $action['url']; + if (!empty($pageUrl)) { + if (!array_key_exists($pageUrl, $this->visitedPageUrls)) { + $this->visitedPageUrls[$pageUrl] = 0; + $profile['totalUniquePageViews']++; + } + $this->visitedPageUrls[$pageUrl]++; + if ($this->visitedPageUrls[$pageUrl] == 2) { + $profile['totalRevisitedPages']++; + } + } + } + + private function handleIfSiteSearchAction($action, &$profile) + { + if (!isset($action['siteSearchKeyword'])) { + return; + } + $keyword = $action['siteSearchKeyword']; + + if (!isset($this->siteSearchKeywords[$keyword])) { + $this->siteSearchKeywords[$keyword] = 0; + ++$profile['totalSearches']; + } + ++$this->siteSearchKeywords[$keyword]; + } + + private function handleSiteSearches(&$profile) + { + // sort by visit/action + arsort($this->siteSearchKeywords); + + foreach ($this->siteSearchKeywords as $keyword => $searchCount) { + $profile['searches'][] = array( + 'keyword' => $keyword, + 'searches' => $searchCount, + ); + } + } + + private function handleIfPageGenerationTime($action, &$profile) + { + if (isset($action['generationTimeMilliseconds'])) { + $this->pageGenerationTimeTotal += $action['generationTimeMilliseconds']; + ++$profile['totalPageViewsWithTiming']; + } + } + + private function handleAveragePageGenerationTime(&$profile) + { + if ($profile['totalPageViewsWithTiming']) { + $profile['averagePageGenerationTime'] = + round($this->pageGenerationTimeTotal / (1000 * $profile['totalPageViewsWithTiming']), $precision = 3); + } + } + + private function handleIfPageLoadTime($action, &$profile) + { + if (isset($action['pageLoadTimeMilliseconds'])) { + $this->pageLoadTimeTotal += $action['pageLoadTimeMilliseconds']; + ++$profile['totalPageViewsWithLoadTime']; + } + } + + private function handleAveragePageLoadTime(&$profile) + { + if ($profile['totalPageViewsWithLoadTime']) { + $profile['averagePageLoadTime'] = + round($this->pageLoadTimeTotal / (1000 * $profile['totalPageViewsWithLoadTime']), $precision = 3); + } + } +} diff --git a/plugins/Actions/config/config.php b/plugins/Actions/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/Actions/config/config.php @@ -0,0 +1,3 @@ +<?php + +return array(); diff --git a/plugins/Actions/config/tracker.php b/plugins/Actions/config/tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/Actions/config/tracker.php @@ -0,0 +1,3 @@ +<?php + +return array(); diff --git a/plugins/Actions/javascripts/actionsDataTable.js b/plugins/Actions/javascripts/actionsDataTable.js new file mode 100644 index 0000000000000000000000000000000000000000..b6760923f86e166769b9dac592360e01d95901c2 --- /dev/null +++ b/plugins/Actions/javascripts/actionsDataTable.js @@ -0,0 +1,341 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +(function ($, require) { + + var exports = require('piwik/UI'), + DataTable = exports.DataTable, + dataTablePrototype = DataTable.prototype; + + // helper function for ActionDataTable + function getLevelFromClass(style) { + if (!style || typeof style == "undefined") return 0; + + var currentLevel = 0; + + var currentLevelIndex = style.indexOf('level'); + if (currentLevelIndex >= 0) { + currentLevel = Number(style.slice(currentLevelIndex + 5, currentLevelIndex + 6)); + } + return currentLevel; + } + + /** + * UI control that handles extra functionality for Actions datatables. + * + * @constructor + */ + exports.ActionsDataTable = function (element) { + this.parentAttributeParent = ''; + this.parentId = ''; + this.disabledRowDom = {}; // to handle double click on '+' row + + DataTable.call(this, element); + }; + + $.extend(exports.ActionsDataTable.prototype, dataTablePrototype, { + + //see dataTable::bindEventsAndApplyStyle + bindEventsAndApplyStyle: function (domElem, rows) { + var self = this; + + self.cleanParams(); + self.preBindEventsAndApplyStyleHook(domElem, rows); + + if (!rows) { + rows = $('tr', domElem); + } + + self.numberOfSubtables = rows.filter('.subDataTable').click(function () { + if (!self.param.filter_pattern_recursive) { + // we don't display the link on the row with subDataTable when we are already + // printing all the subTables (case of recursive search when the content is + // including recursively all the subtables + self.onClickActionSubDataTable(this); + } + }).length; + self.applyCosmetics(domElem, rows); + self.handleColumnHighlighting(domElem); + self.handleRowActions(domElem, rows); + self.handleLimit(domElem); + self.handlePeriod(domElem); + self.handleAnnotationsButton(domElem); + self.handleExportBox(domElem); + self.handleSort(domElem); + self.handleOffsetInformation(domElem); + if (self.workingDivId != undefined) { + var dataTableLoadedProxy = function (response) { + self.dataTableLoaded(response, self.workingDivId); + }; + + self.handleConfigurationBox(domElem, dataTableLoadedProxy); + self.handleSearchBox(domElem, dataTableLoadedProxy); + } + + self.handleColumnDocumentation(domElem); + self.handleRelatedReports(domElem); + self.handleTriggeredEvents(domElem); + self.handleCellTooltips(domElem); + self.setFixWidthToMakeEllipsisWork(domElem); + self.handleSummaryRow(domElem); + self.openSubtableFromLevel0IfOnlyOneSubtableGiven(domElem); + self.postBindEventsAndApplyStyleHook(domElem, rows); + }, + + openSubtableFromLevel0IfOnlyOneSubtableGiven: function (domElem) { + var $subtables = domElem.find('.subDataTable'); + var hasOnlyOneSubtable = $subtables.length === 1; + + if (hasOnlyOneSubtable) { + var hasOnlyOneRow = domElem.find('tbody tr.level0').length === 1; + + if (hasOnlyOneRow) { + var $labels = $subtables.find('.label'); + if ($labels.length) { + $labels.first().click(); + } + } + } + }, + + openSubtableFromSubtableIfOnlyOneSubtableGiven: function (domElem) { + var hasOnlyOneRow = domElem.length === 1; + var hasOnlyOneSubtable = domElem.hasClass('subDataTable'); + + if (hasOnlyOneRow && hasOnlyOneSubtable) { + // when subtable is loaded + var $labels = domElem.find('.label'); + if ($labels.length) { + $labels.first().click(); + } + } + }, + + //see dataTable::applyCosmetics + applyCosmetics: function (domElem, rows) { + var self = this; + var rowsWithSubtables = rows.filter('.subDataTable'); + + rowsWithSubtables.css('font-weight', 'bold'); + + $("th:first-child", domElem).addClass('label'); + $(rowsWithSubtables) + .each(function () { + if (self.param.filter_pattern_recursive) { + $(this).addClass('expanded'); + // remove tooltip "Click this row to expand or contract the subtable" + $(this).attr('title', ''); + } + }); + + var rootRow = rows.first().prev(); + + // we look at the style of the row before the new rows to determine the rows' + // level + var level = rootRow.length ? getLevelFromClass(rootRow.attr('class')) + 1 : 0; + + rows.each(function () { + var currentStyle = $(this).attr('class') || ''; + + if (currentStyle.indexOf('level') == -1) { + $(this).addClass('level' + level); + } + + // we add an attribute parent that contains the ID of all the parent categories + // this ID is used when collapsing a parent row, it searches for all children rows + // which 'parent' attribute's value contains the collapsed row ID + $(this).prop('parent', function () { + return self.parentAttributeParent + ' ' + self.parentId; + }); + }); + + self.addOddAndEvenClasses(domElem); + }, + + addOddAndEvenClasses: function(domElem) { + // empty + }, + + handleRowActions: function (domElem, rows) { + this.doHandleRowActions(rows); + }, + + // Called when the user click on an actionDataTable row + onClickActionSubDataTable: function (domElem) { + var self = this; + + // get the idSubTable + var idSubTable = $(domElem).attr('id'); + + var divIdToReplaceWithSubTable = 'subDataTable_' + idSubTable; + + var $insertAfter = $(domElem).nextUntil(':not(.comparePeriod):not(.comparisonRow)').last(); + if (!$insertAfter.length) { + $insertAfter = $(domElem); + } + + var NextStyle = $insertAfter.next().attr('class'); + var CurrentStyle = $(domElem).attr('class'); + + var currentRowLevel = getLevelFromClass(CurrentStyle); + var nextRowLevel = getLevelFromClass(NextStyle); + + // if the row has not been clicked + // which is the same as saying that the next row level is equal or less than the current row + // because when we click a row the level of the next rows is higher (level2 row gives level3 rows) + if (currentRowLevel >= nextRowLevel) { + //unbind click to avoid double click problem + $(domElem).off('click'); + self.disabledRowDom = $(domElem); + + var numberOfColumns = $(domElem).children().length; + $insertAfter.after('\ + <tr id="' + divIdToReplaceWithSubTable + '" class="cellSubDataTable">\ + <td colspan="' + numberOfColumns + '">\ + <span class="loadingPiwik" style="display:inline">\ + <span class="matomo-loader"><span></span><span></span><span></span></span>\ + Loading...\ + </span>\ + </td>\ + </tr>\ + '); + var savedActionVariable = self.param.action; + + // reset all the filters from the Parent table + var filtersToRestore = self.resetAllFilters(); + + // Do not reset the sorting filters that must be applied to sub tables + this.param['filter_sort_column'] = filtersToRestore['filter_sort_column']; + this.param['filter_sort_order'] = filtersToRestore['filter_sort_order']; + this.param['enable_filter_excludelowpop'] = filtersToRestore['enable_filter_excludelowpop']; + + self.param.idSubtable = idSubTable; + self.param.action = self.props.subtable_controller_action; + + var extraParams = {}; + extraParams.comparisonIdSubtables = self.getComparisonIdSubtables($(domElem)); + + self.reloadAjaxDataTable(false, function (resp) { + self.actionsSubDataTableLoaded(resp, idSubTable); + self.repositionRowActions($(domElem)); + }, extraParams); + self.param.action = savedActionVariable; + + self.restoreAllFilters(filtersToRestore); + + delete self.param.idSubtable; + } + // else we toggle all these rows + else { + var isExpanded = $(domElem).hasClass('subDataTable') && $(domElem).hasClass('expanded'); + + $(domElem).siblings().each(function () { + var parents = $(this).prop('parent').split(' '); + if (parents) { + if (parents.indexOf(idSubTable) >= 0 + || parents.indexOf('subDataTable_' + idSubTable) >= 0) { + if (!isExpanded) { + $(this).css('display', '').removeClass('hidden'); + + // unroll everything if the row is already opened + var NextStyle = $(this).next().attr('class'); + var CurrentStyle = $(this).attr('class'); + + var currentRowLevel = getLevelFromClass(CurrentStyle); + var nextRowLevel = getLevelFromClass(NextStyle); + + if (currentRowLevel < nextRowLevel) { + $(this).addClass('expanded'); + } + } + else { + $(this).css('display', 'none').addClass('hidden'); + } + self.repositionRowActions($(domElem)); + } + } + }); + + self.$element.trigger('piwik:actionsSubTableToggled'); + } + + // toggle the +/- image + $(domElem).toggleClass('expanded'); + }, + + //called when the full table actions is loaded + dataTableLoaded: function (response, workingDivId) { + var content = $(response); + var idToReplace = workingDivId || $(content).attr('id'); + + //reset parents id + self.parentAttributeParent = ''; + self.parentId = ''; + + var dataTableSel = $('#' + idToReplace); + + // keep the original list of related reports + var oldReportsElem = $('.datatableRelatedReports', dataTableSel); + $('.datatableRelatedReports', content).replaceWith(oldReportsElem); + + dataTableSel.replaceWith(content); + + content.trigger('piwik:dataTableLoaded'); + + piwikHelper.compileVueEntryComponents(content); + + piwikHelper.lazyScrollTo(content[0], 400); + + return content; + }, + + // Called when a set of rows for a category of actions is loaded + actionsSubDataTableLoaded: function (response, idSubTable) { + var self = this; + var idToReplace = 'subDataTable_' + idSubTable; + var root = $('#' + self.workingDivId); + + var response = $(response); + self.parentAttributeParent = $('tr#' + idToReplace).prev().prop('parent'); + self.parentId = idToReplace; + + $('tr#' + idToReplace, root).after(response).remove(); + + var requiredColumnCount = 0, availableColumnCount = 0; + + response.prev().find('td').each(function(){ requiredColumnCount += $(this).attr('colspan') || 1; }); + response.find('td').each(function(){ availableColumnCount += $(this).attr('colspan') || 1; }); + + var missingColumns = requiredColumnCount - availableColumnCount; + for (var i = 0; i < missingColumns; i++) { + // if the subtable has fewer columns than the parent table, add some columns. + // this happens for example, when the parent table has performance metrics and the subtable doesn't. + response.append('<td>-</td>'); + } + + var re = /subDataTable_(\d+)/; + var ok = re.exec(self.parentId); + if (ok) { + self.parentId = ok[1]; + } + + // we execute the bindDataTableEvent function for the new DIV + self.bindEventsAndApplyStyle($('#' + self.workingDivId), response); + + self.$element.trigger('piwik:actionsSubDataTableLoaded'); + + //bind back the click event (disabled to avoid double-click problem) + self.disabledRowDom.click( + function () { + self.onClickActionSubDataTable(this) + }); + + self.openSubtableFromSubtableIfOnlyOneSubtableGiven(response); + } + }); + +})(jQuery, require); diff --git a/plugins/Actions/javascripts/rowactions.js b/plugins/Actions/javascripts/rowactions.js new file mode 100644 index 0000000000000000000000000000000000000000..71b2be68854384d93879b2310c6ecc23bf94e78d --- /dev/null +++ b/plugins/Actions/javascripts/rowactions.js @@ -0,0 +1,99 @@ +$(function () { + + function isActionsModule(params) + { + return params.module == 'Actions'; + } + + function isPageUrlReport(params) { + var action = params.action; + + return isActionsModule(params) && + (action == 'getPageUrls' || action == 'getEntryPageUrls' || action == 'getExitPageUrls' || action == 'getPageUrlsFollowingSiteSearch'); + }; + + function isPageTitleReport(params) { + var action = params.action; + + return isActionsModule(params) && (action == 'getPageTitles' || action == 'getPageTitlesFollowingSiteSearch'); + }; + + function getLinkForTransitionAndOverlayPopover(tr) + { + tr = getRealRowIfComparisonRow(tr); + + var link = tr.find('> td:first > a').attr('href'); + // replace all &, that are not part of a named character reference with a tailing semicolon, with a & + // otherwise named character references without a tailing , (like ®) would be replaced + link = link.replace(/&([a-z]+[^a-z;])/, '&$1'); + link = $('<textarea>').html(link).val(); // remove html entities + return link; + } + + if (window.DataTable_RowActions_Transitions) { + DataTable_RowActions_Transitions.registerReport({ + isAvailableOnReport: function (dataTableParams) { + return isPageUrlReport(dataTableParams); + }, + isAvailableOnRow: function (dataTableParams, tr) { + tr = getRealRowIfComparisonRow(tr); + return isPageUrlReport(dataTableParams) && tr.find('> td:first span.label').parent().is('a') + }, + trigger: function (tr, e, subTableLabel, originalRow) { + var overrideParams = $.extend({}, $(originalRow || tr).data('param-override')); + if (typeof overrideParams !== 'object') { + overrideParams = {}; + } + + tr = getRealRowIfComparisonRow(tr); + + var link = getLinkForTransitionAndOverlayPopover(tr); + var popoverUrl = 'url:' + link; + + Object.keys(overrideParams).forEach(function (paramName) { + if (!overrideParams[paramName]) { + return; + } + + popoverUrl += ':' + encodeURIComponent(paramName) + ':' + encodeURIComponent(overrideParams[paramName]); + }); + + this.openPopover(popoverUrl); + } + }); + + DataTable_RowActions_Transitions.registerReport({ + isAvailableOnReport: function (dataTableParams) { + return isPageTitleReport(dataTableParams); + }, + isAvailableOnRow: function (dataTableParams, tr) { + return isPageTitleReport(dataTableParams); + }, + trigger: function (tr, e, subTableLabel) { + DataTable_RowAction.prototype.trigger.apply(this, [tr, e, subTableLabel]); + } + }); + } + + if (window.DataTable_RowActions_Overlay) { + DataTable_RowActions_Overlay.registerReport({ + isAvailableOnReport: function (dataTableParams) { + return isPageUrlReport(dataTableParams); + }, + onClick: function (actionA, tr, e) { + return { + link: getLinkForTransitionAndOverlayPopover(tr), + segment: null + } + } + }); + } + + function getRealRowIfComparisonRow(tr) { + if (tr.is('.comparisonRow')) { + var prevUntil = tr.prevUntil('.parentComparisonRow').prev(); + return prevUntil.length ? prevUntil : tr.prev(); + } + return tr; + } +}); \ No newline at end of file diff --git a/plugins/Actions/lang/am.json b/plugins/Actions/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..5916ff3631a7d91cac106b4aa51e0219ff4b34a2 --- /dev/null +++ b/plugins/Actions/lang/am.json @@ -0,0 +1,9 @@ +{ + "Actions": { + "ColumnClicks": "ጠቃድርጎች", + "ColumnDownloadURL": "የማውረጃ ዩ አር ኤል", + "ColumnPageName": "የገፅ ስም", + "ColumnUniqueClicks": "ብቸኛ ጠቅአድርጎች", + "ColumnUniqueDownloads": "ብቸኛ የወረዱ" + } +} diff --git a/plugins/Actions/lang/ar.json b/plugins/Actions/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..1320a8a2393a72cb41f84727ff93012d663ea758 --- /dev/null +++ b/plugins/Actions/lang/ar.json @@ -0,0 +1,63 @@ +{ + "Actions": { + "ActionType": "نوع الأمر", + "AvgGenerationTimeTooltip": "متوسط على أساس %1$s كبسة %2$s ما بين %3$s و %4$s", + "ColumnActionURL": "رابط الأمر", + "ColumnClicks": "النقرات", + "ColumnClicksDocumentation": "عدد مرات النقر على هذا الرابط.", + "ColumnDownloadURL": "رابط التحميل", + "ColumnEntryPageTitle": "عنوان صفحة الوصول", + "ColumnEntryPageURL": "رابط صفحة الوصول", + "ColumnExitPageTitle": "عنوان صفحة المغادرة", + "ColumnExitPageURL": "رابط صفحة المغادرة", + "ColumnNoResultKeyword": "الكلمات الدلالية بدون نتائج بحث", + "ColumnPageName": "اسم الصفحة", + "ColumnPageURL": "رابط الصفحة", + "ColumnPagesPerSearch": "صفحات نتائج البحث", + "ColumnPagesPerSearchDocumentation": "سيقوم الزوار بالبحث على صفحات موقعك، وفي بعض الأحيان سينقرون \"التالي\" لمشاهدة المزيد من النتائج. هذا هو متوسط عدد صفحات نتائج البحث المعروضة لتلك الكلمة الدلالية.", + "ColumnSearchCategory": "فئة البحث", + "ColumnSearchExitsDocumentation": "نسبة الزيارات التي غادرت موقعك بعد البحث عن هذه الكلمة باستخدام محرك بحث موقعك.", + "ColumnSearchResultsCount": "عدد نتائج البحث", + "ColumnSearches": "البجث", + "ColumnSearchesDocumentation": "عدد الزيارات التي تم فيها البحث عن هذه الكلمة باستخدام محرك بحث موقعك.", + "ColumnSiteSearchKeywords": "الكلمات الدلالية الفريدة", + "ColumnUniqueClicks": "النقرات الفريدة", + "ColumnUniqueClicksDocumentation": "عدد الزيارات التي تم النقر فيها على هذا الرابط. إذا تم النقر عدة مرات على الرابط نفسه في زيارة واحدة، ستحتسب جميعها واحدة فقط.", + "ColumnUniqueDownloads": "التحميلات الفريدة", + "ColumnUniqueOutlinks": "الروابط الصادرة الفريدة", + "DownloadsReportDocumentation": "في هذا التقرير، يمكنك مشاهدة أي الملفات قام زوارك بتحميلها. %s ما يحتسبه بايويك كعملية تحميل هو النقر على رابط التحميل. لا يستطيع أن يعلم بايويك ما إذا كان التحميل قد اكتمل أم لا.", + "EntryPageTitles": "عناوين صفحات الوصول", + "EntryPageTitlesReportDocumentation": "يتضمن هذا التقرير معلومات عن عناوين صفحات الوصول التي تم استخدامها خلال فترة زمنية معينة.", + "ExitPageTitles": "عناوين صفحات الخروج", + "ExitPageTitlesReportDocumentation": "يتضمن هذا التقرير معلومات عن عناوين صفحات الخروج التي حدثت خلال الفترة المحددة.", + "OneSearch": "نتيجة بحث واحدة", + "OutlinkDocumentation": "الرابط الصادر هو رابط يقود الزائر بعيداً عن موقعك (إلى اسم نطاق آخر).", + "OutlinksReportDocumentation": "يعرض هذا التقرير قائمة هيكلية بالروابط الصادرة والتي تم النقر عليها بواسطة زوارك.", + "PageTitlesReportDocumentation": "يتضمن هذا التقرير معلومات عن عناوين الصفحات التي تم زيارتها. %1$s عنوان الصفحة هو وسم لغة %2$s HTML الذي تعرضه أغلب متصفحات ويب كعناون النافذة.", + "PageUrls": "روابط الصفحة", + "PagesReportDocumentation": "يتضمن هذا التقرير معلومات عن روابط الصفحات التي تم زيارتها. %s الجدول منظم هيكلياً، والروابط معروضة في شكل شجرة مجلدات.", + "PagesSubcategoryHelp3": "استخدم أيقونات زائد وناقص على اليمين للتنقل.", + "PluginDescription": "تقارير عرض الصفحات وعناوين الصفحات. تتيح لك هذه التقارير قياس خدمة البحث الداخلية في موقعك. كما تقدم تتبعاً آلياً للنقرات للروابط الخارجية وتنزيلات الملفات.", + "SiteSearchCategories1": "يعرض هذا التقرير قائمة الفئات التي اختارها زوارك عندما قاموا بالبحث في موقعك.", + "SiteSearchCategories2": "على سبيل المثال، المواقع التجارية عادة ما تحتوي طريقة لاختيار \"فئة\" بحيث يمكن للزوار حصر البحث في فئة منتجات معينة.", + "SiteSearchFollowingPagesDoc": "عندما يقوم زوارك بالبحث في موقعك، فإنهم يبحثون عن صفحة بعينها، مقال ما، منتج أو خدمة. هذا التقرير يعرض أكثر الصفحات التي قاموا بالنقر عليها عند استخدام بحث الموقع. بكلمات أخرى، هذه قائمة بالصفحات التي يبحث عنها زوارك دائماً.", + "SiteSearchIntro": "تتبع عمليات البحث التي يقوم بها الزوار على موقعك هي أكثر الطرق فعالية لمعرفة ما يبحث عنه جمهورك، هذا قد يساعد على البحث عن أفكار للمحتوى الجديد، سلع جديد للتسوق الألكتروني التي بحث عنها المستهلكين، و عموما تحسين تجربة الزوار على موقعك.", + "SiteSearchKeyword": "كلمة (بحث موقع(", + "SiteSearchKeywordsDocumentation": "يعرض هذا التقرير قوائم كلمات البحث الدلالية التي بحث عنها زوارك باستخدام محرك البحث الداخلي لديك.", + "SiteSearchKeywordsNoResultDocumentation": "يعرض هذا التقرير قوائك كلمات البحث الدلالية التي لم تأتي بأي نتائج: ربما تحتاج خوارزمية محرك البحث إلى التحسين، أو ربما بحث زوار موقعك عن محتوى غير موجود (بعد)؟", + "SubmenuPageTitles": "عناوين الصفحات", + "SubmenuPagesEntry": "صفحات الدخول", + "SubmenuPagesExit": "صفحات الخروج", + "SubmenuSitesearch": "بحث الموقع", + "WidgetEntryPageTitles": "عناوين صفحات الدخول", + "WidgetExitPageTitles": "عناوين صفحات الخروج", + "WidgetPageTitles": "عناوين الصفحة", + "WidgetPageTitlesFollowingSearch": "عناوين الصفحة بعد بحث بالموقع", + "WidgetPageUrlsFollowingSearch": "الصفحات بعد بحث بالموقع", + "WidgetPagesEntry": "صفحات الوصول", + "WidgetPagesExit": "صفحات الخروج", + "WidgetSearchCategories": "فئات البحث", + "WidgetSearchKeywords": "كلمات البحث الدلالية", + "WidgetSearchNoResultKeywords": "كلمات البحث الدلالية بلا نتائج" + } +} diff --git a/plugins/Actions/lang/az.json b/plugins/Actions/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..c87bf51c1605d7ec928b929d6ca71e2901448b41 --- /dev/null +++ b/plugins/Actions/lang/az.json @@ -0,0 +1,14 @@ +{ + "Actions": { + "ColumnClicks": "Klikləmələr", + "ColumnClicksDocumentation": "Bu keçidin kliklənmə sayı.", + "ColumnDownloadURL": "İndirme Sayfa Adresleri", + "ColumnEntryPageTitle": "Giriş səhifəsi Başlığı", + "ColumnEntryPageURL": "Giriş Sayfa Adresi", + "ColumnExitPageTitle": "Çıxış səhifəsi Başlığı", + "ColumnExitPageURL": "Çıxış Sayfa Adresi", + "ColumnPageName": "Səhifə Adı", + "ColumnPageURL": "Sayfa Adresi", + "ColumnSearchCategory": "Axtarış Kategorisi" + } +} diff --git a/plugins/Actions/lang/be.json b/plugins/Actions/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..26322c5b8f4735f1992f04f7761dc110d128f52b --- /dev/null +++ b/plugins/Actions/lang/be.json @@ -0,0 +1,25 @@ +{ + "Actions": { + "ColumnClicks": "Націскі", + "ColumnClicksDocumentation": "Колькасць націскаў па гэтай спасылцы.", + "ColumnDownloadURL": "URL запамповак", + "ColumnEntryPageTitle": "Назва старонкі ўваходу", + "ColumnEntryPageURL": "URL старонкі ўваходу", + "ColumnExitPageTitle": "Назва старонак выхаду", + "ColumnExitPageURL": "URL старонкі выхаду", + "ColumnPageName": "Імя старонкі", + "ColumnPageURL": "URL старонкі", + "ColumnUniqueClicks": "Унікальныя націскі", + "ColumnUniqueClicksDocumentation": "Колькасць наведвалнікаў, якія націснулі на гэтую спасылку. Калі спасылка была націснутая некалькі разоў на працягу аднаго наведвання, то яна лічыцца адзін раз.", + "ColumnUniqueDownloads": "Унікальныя запампоўкі", + "DownloadsReportDocumentation": "На гэтай справаздачы, можна ўбачыць, якія файлы былі спампаваны вашымя наведвальнікамі. %s Matomo лічыць за спампаўку націск на адпаведную спасылку. Была запампоўка завершана ці не, гэта не вядома Matomo.", + "OutlinkDocumentation": "Знешняя спасылка - гэта такая спасылка, якая вядзе наведвальніка прэч з вашага сайта (да іншага дамену).", + "OutlinksReportDocumentation": "Гэтая справаздача паказвае іерархічны спіс URL-адрасоў знешніх спасылак, якія былі націснуты вашымі наведвальнікамі.", + "PageTitlesReportDocumentation": "Гэтая справаздача змяшчае інфармацыю аб назвах старонак, якія былі наведаны. %1$s Назва старонкі гэта HTML %2$s тэг, які большасць брасзэраў паказваюць у загалоўке акна.", + "PagesReportDocumentation": "Гэтая справаздача змяшчае інфармацыю аб URL-адрасах старонак, якія былі наведаны. %s Табліца арганізавана іерархічна, URL-адрасы адлюстроўваюцца ў выглядзе тэчакавай структуры.", + "PagesSubcategoryHelp3": "Выкарыстоўвайце значкі плюса і мінуса, якія знаходзяцца злева, для навігацыі.", + "SubmenuPageTitles": "Назвы старонак", + "SubmenuPagesEntry": "Старонкі ўваходу", + "SubmenuPagesExit": "Старонкі выхаду" + } +} diff --git a/plugins/Actions/lang/bg.json b/plugins/Actions/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..dcfe3097b0f2a3d94bcaa74eeb46c9f016f680f3 --- /dev/null +++ b/plugins/Actions/lang/bg.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Тип действие", + "ActionTypes": "Видове действия", + "ActionsInVisit": "Действия при посещение", + "AvgGenerationTimeTooltip": "Средно базирано на %1$s посещение(я) %2$s между %3$s и %4$s", + "Behaviour": "Поведение", + "ColumnActionURL": "URL адрес на действие", + "ColumnClickedURL": "Кликната препратка", + "ColumnClickedURLs": "Кликнати препратки", + "ColumnClicks": "Кликвания", + "ColumnClicksDocumentation": "Брой кликвания на тази връзка.", + "ColumnDownloadURL": "Свален URL", + "ColumnDownloadURLs": "URL адреси за изтегляне", + "ColumnEntryPageTitle": "Входно заглавие", + "ColumnEntryPageURL": "Входна страница", + "ColumnEntryPageURLs": "URL адреси на входни страници", + "ColumnExitPageTitle": "Изходно заглавие", + "ColumnExitPageURL": "Изходна страница", + "ColumnExitPageURLs": "URL адреси на изходни страници", + "ColumnIdPageview": "ID на преглед на страница", + "ColumnInteractionPosition": "Позиция на взаимодействие", + "ColumnNoResultKeyword": "Ключова дума без резултати в търсенето", + "ColumnPageName": "Име на страницата", + "ColumnPageURL": "Страница URL", + "ColumnPageViewPosition": "Позиция на прегледа на страницата", + "ColumnPagesPerSearch": "Страници с резултати от търсене", + "ColumnPagesPerSearchDocumentation": "Посетителите ще търсят във вашия сайт, а понякога избират „Напред“, за да видят повече резултати. Това е средният брой на страниците с резултати от търсенето, видян за тази ключова дума.", + "ColumnSearchCategory": "Търсена категория", + "ColumnSearchExits": "%% Изходи за търсене", + "ColumnSearchExitsDocumentation": "Процент посещения, след които, потребителите са напуснали сайта, при търсене за тази ключова дума.", + "ColumnSearchResultsCount": "Брой резултати от търсенето", + "ColumnSearches": "Търсачки", + "ColumnSearchesDocumentation": "Брой посещения търсениза тази ключова дума от системата за търсене на твоя сайт.", + "ColumnSiteSearchKeywords": "Уникални ключови думи", + "ColumnUniqueClicks": "Уникални кликове", + "ColumnUniqueClicksDocumentation": "Броят на посещенията които са кликнали на тази връзка. Ако на тази връзка е било кликано няколко пъти, при едно посещение, отчита се само веднъж.", + "ColumnUniqueDownloads": "Уникални изтегляния", + "ColumnUniqueOutlinks": "Уникални изходящи", + "DownloadsReportDocumentation": "В този отчет можете да видите файловете, които Вашите посетители са изтеглили. %s Matomo отчита броя кликвания върху връзката за сваляне. Дали изтеглянето е завършено или не, не е ясно за Matomo.", + "DownloadsSubcategoryHelp1": "В този отчет можете да видите кои файлове са изтеглили посетителите Ви.", + "DownloadsSubcategoryHelp2": "Това, което Matomo брои за изтегляне, е кликването върху връзка за изтегляне. Дали изтеглянето е завършено или не, не е известно на Matomo.", + "EntryPageTitles": "Входни заглавия", + "EntryPageTitlesReportDocumentation": "Този отчет съдържа информация за заглавията на входните страници, които са били използвани през определен период.", + "EntryPagesReportDocumentation": "Този отчет съдържа информация за входните страници, които са били използвани през посочения период. Входната страница е първата страница, която потребителят преглежда по време на посещението си. %s Входните URL адреси се показват като структура от папки.", + "EntryPagesSubcategoryHelp1": "Този отчет съдържа информация за входните страници, които са били използвани през посочения период. Входната страница е първата страница, която потребителят преглежда по време на посещението си.", + "EntryPagesSubcategoryHelp2": "Входните URL адреси се показват като структура от папки.", + "ExitPageTitles": "Изходни заглавия", + "ExitPageTitlesReportDocumentation": "Този отчет съдържа информация за заглавията на изходните страници, които са били използвани през определен период.", + "ExitPagesReportDocumentation": "Този отчет съдържа информация за изходните страници, възникнали през посочения период. Изходната страница е последната страница, която потребителят преглежда по време на посещението си. %s Изходните URL адреси се показват като структура от папки.", + "ExitPagesSubcategoryHelp1": "Този отчет съдържа информация за изходните страници, възникнали през посочения период. Изходната страница е последната страница, която потребителят преглежда по време на посещението си.", + "ExitPagesSubcategoryHelp2": "Изходните URL адреси се показват като структура от папки.", + "MainMetricsReportDocumentation": "Този отчет предоставя много основен преглед на това какви действия предприемат посетителите на уебсайта Ви.", + "OneSearch": "1 търсене", + "OutlinkDocumentation": "Изходящата връзка е връзка, която води посетителя извън Вашия сайт (към друг домейн).", + "OutlinksReportDocumentation": "Този доклад показва йерархичен списък на изходящите адреси, които са избрани от посетителите.", + "OutlinksSubcategoryHelp1": "Този отчет показва йерархичен списък с URL адреси за препращане, върху които са кликнали посетителите Ви. Препратка (outlink) е връзка, която отвежда посетителя извън Вашия уебсайт (към друг домейн).", + "PageTitlesReportDocumentation": "Този доклад включва информация за заглавията на посетена страница. %1$s Заглавието на страницата е HTML %2$s таг, който се изобразява в заглавната част на повечето браузери.", + "PageTitlesSubcategoryHelp1": "Този отчет съдържа информация за заглавията на посетените страници.", + "PageTitlesSubcategoryHelp2": "Заглавието на страницата е HTML &lt;title&gt; таг, който повечето браузъри показват в заглавието на прозореца си.", + "PageUrls": "URLs страница", + "PageViewsByVisitor": "Колко пъти тази страница е била прегледана от този посетител за всички посещения", + "PagesReportDocumentation": "Този доклад включва информация за посетените връзки. %s Таблицата е организирана йерархично, връзките са показани като структура от папки.", + "PagesSubcategoryHelp1": "Този отчет съдържа информация за URL адресите на страниците, които са били посетени.", + "PagesSubcategoryHelp2": "Таблицата е организирана йерархично, URL адресите се показват като структура от папки.", + "PagesSubcategoryHelp3": "Използвайте знаците плюс и минус за навигация.", + "PluginDescription": "Отчети за показванията на страниците и заглавията на страниците. Позволява Ви да измерите търсачката на Вашия вътрешен уебсайт. Автоматично проследява кликванията върху външни връзки и изтегляния на файлове.", + "RevisitedPages": "Страници, преглеждани повече от веднъж", + "SiteSearchCategories": "Категории (търсене в сайта)", + "SiteSearchCategories1": "Този доклад показва списък с категориите, които посетителите са избрали при търсене във Вашия сайт.", + "SiteSearchCategories2": "Например, електронните магазини обикновено имат категоризатор, така че посетителите да могат да филтрират търсенията си за всички продукти по категория.", + "SiteSearchCategory": "Категория (търсене в сайта)", + "SiteSearchFollowingPagesDoc": "Когато посетителите разглеждат Вашия сайт, те търсят определена страница, продукт или услуга. Този доклад показва списъка със страници, които са били посетени предимно след използване на търсачката на сайта. С други думи, това е списъкът с най-посещаваните страници, които са били търсени от посетителите намиращи се във Вашия сайт.", + "SiteSearchIntro": "Проследявайки търсенията, които посетителите правят на вашия сайт, е един много ефективен начин да научите повече за това, което вашата аудитория търси. Това може да помогне за намиране на идеи за ново съдържание, нови продукти и търговия, които потенциалните клиенти могат да търсят, и като цяло да подобри впечатлението на потребителя от вашия сайт.", + "SiteSearchKeyword": "Ключова дума (Търсене на сайта)", + "SiteSearchKeywordCount": "Брой ключови думи (търсене в сайта)", + "SiteSearchKeywordCounts": "Брой ключови думи (търсене в сайта)", + "SiteSearchKeywords": "Ключови думи (търсене в сайта)", + "SiteSearchKeywordsDocumentation": "Този отчет описва ключовите думи, които посетителите търсят през търсачката на сайта.", + "SiteSearchKeywordsNoResultDocumentation": "Този отчет съдържа „Търсене по ключови думи“, в който няма резултати от търсенето: може би алгоритъмът на търсачката може да се подобри, или може би вашите посетители търсят съдържание, което (все още) не е на вашия сайт?", + "SiteSearchSubcategoryHelp1": "Секцията за търсене в сайта показва кои ключови думи използват посетителите, когато търсят във Вашия уебсайт. Той също така показва кои страници преглеждат потребителите след извършване на търсене и кои ключови думи за търсене на място не връщат никакви резултати.", + "SiteSearchSubcategoryHelp2": "Тези отчети могат да Ви дадат идеи за липсващо съдържание на Вашия сайт, представа за това, което Вашите посетители търсят, но не могат да намерят лесно и др.", + "SiteSearchSubcategoryHelp3": "Научете повече в ръководството за търсене в сайта.", + "SubmenuPageTitles": "Заглавия на страници", + "SubmenuPagesEntry": "Входни страници", + "SubmenuPagesExit": "Изходни страници", + "SubmenuSitesearch": "Търсене на сайт", + "TimeSpentInReferringAction": "Време, прекарано в препращащо действие", + "WidgetEntryPageTitles": "Входящо заглавие на страницата", + "WidgetExitPageTitles": "Изходящо заглавие на страницата", + "WidgetPageTitles": "Заглавия на страниците", + "WidgetPageTitlesFollowingSearch": "Заглавия на страници следящи търсенията на сайтове", + "WidgetPageUrlsFollowingSearch": "Страници следящи търсенията на сайтове", + "WidgetPagesEntry": "Входящи страници", + "WidgetPagesExit": "Изходящи страници", + "WidgetSearchCategories": "Търси категории", + "WidgetSearchKeywords": "Ключови думи търсени от сайта", + "WidgetSearchNoResultKeywords": "Търсене по ключови думи без резултати" + } +} diff --git a/plugins/Actions/lang/bn.json b/plugins/Actions/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Actions/lang/bn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Actions/lang/bs.json b/plugins/Actions/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..25cfff8e9f128c58adba53a79aad7f12f31e38d7 --- /dev/null +++ b/plugins/Actions/lang/bs.json @@ -0,0 +1,60 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "Prosijek baziran na %1$s pogodak(a) %2$s između %3$s i %4$s", + "ColumnClicks": "Klikovi", + "ColumnClicksDocumentation": "Broj klikova na ovaj link", + "ColumnDownloadURL": "Download URL", + "ColumnEntryPageTitle": "Naslov ulazne stranice", + "ColumnEntryPageURL": "URL ulazne stranice", + "ColumnExitPageTitle": "Naslov izlazne stranice", + "ColumnExitPageURL": "URL izlazne stranice", + "ColumnNoResultKeyword": "Riječ bez rezultata u pretrazi", + "ColumnPageName": "Ime stranice", + "ColumnPageURL": "URL stranice", + "ColumnPagesPerSearch": "Pretraži stranice rezultata", + "ColumnPagesPerSearchDocumentation": "Posjetioci će pretraživati vašu web-stranicu, i ponekada kliknuti na \"sljedeće\" da bi dobili više rezultata. Ovo je prosječan broj rezultata u pretrazi koje se prikazuju za ovu riječ.", + "ColumnSearchCategory": "Kategorija pretrage", + "ColumnSearchExitsDocumentation": "Percentaža posjeta kod kojih je stranica napuštena poslije pretrage za ovu riječ, izvršeno preko vašeg pretraživača.", + "ColumnSearchResultsCount": "Broj rezultata u pretragama", + "ColumnSearches": "Pretrage", + "ColumnSearchesDocumentation": "Broj posjeta u kojima je tražena ova riječ preko vaše stranice.", + "ColumnSiteSearchKeywords": "Unikatne riječi", + "ColumnUniqueClicks": "Jedinstveni klikovi", + "ColumnUniqueClicksDocumentation": "Broj posjeta koji su umiješan sa klikom na ovaj link. Ako je link kliknut više puta prilikom posjete, uračunat je samo jednom.", + "ColumnUniqueDownloads": "Jedinstveni downloadi", + "ColumnUniqueOutlinks": "Jedinstveni izlazni linkovi", + "DownloadsReportDocumentation": "U ovom izvještaju možete vidjeti koje fajlove je posjetioc skinuo. %s je šta Matomo računa kao download je klik na download link. Da li je download kompletiran, Matomo ne zna.", + "EntryPageTitles": "Naslovi ulaznih stranica", + "EntryPageTitlesReportDocumentation": "Ovaj izvještaj sadrži informacije o naslovima ulaznih stranica koje su korištene prilikom specifičnog vremena.", + "ExitPageTitles": "Naslovi izlaznih stranica", + "ExitPageTitlesReportDocumentation": "Ovaj izvještaj sadrži informacije o naslovima izlaznih stranica koji su se dogodili prilikom specifičnom perioda.", + "OneSearch": "1 traži", + "OutlinkDocumentation": "Izlazni link je link na kojeg je izašao posjetilac sa tvoje stranica na neki drugi websajt (druga domena)", + "OutlinksReportDocumentation": "Ovaj izvještaj sadrži hijerarhalnu listu izlaznih linkova koji su kliknuti od strane tvojih posjetilaca.", + "PageTitlesReportDocumentation": "Ovaj raport sadrži informacije o naslovima stranica koje su posjećene. %1$s Naslov stranice je HTML %2$s Oznaka koju većina pregledača prikazuje u njihovom prozornom imenu.", + "PageUrls": "Veze stranice", + "PagesReportDocumentation": "Ovaj izvještaj sadrži informacije o stranicama koje su posjećene. %s tabela je organizovana hijerarhijski, URL-ovi su prikazani kao struktura foldera.", + "PagesSubcategoryHelp3": "Koriste plus i minus ikone za navigaciju.", + "SiteSearchCategories1": "Ovaj raport prikazuje spisak kategorija koje su posjetioci odabrali kad su vršili istragu na vašoj stranici.", + "SiteSearchCategories2": "Na primjer, stranice sa elektronskom trgovinom tipično imaju birač kategorija tako da se posjetioci mogu ograničiti na pretrage na svim proizvodima u specifičnoj kategoriji.", + "SiteSearchFollowingPagesDoc": "Kada posjetioci traže na vašoj stranici, oni su u potrazi za određenu stranicu, sadržaj, proizvod, ili uslugu. Ovaj raport prikazuje spisak stranica koje su najviše kliknute poslije interne pretrage. S drugim riječima, ovo je spisak stranica koje su posjetioci najčešće pretraživali na vašoj stranici.", + "SiteSearchIntro": "Praćenje pretraga koje vrše vaši posjetioci je efikasan način shvatanja šta vaši posjetioci traže, pomaže u iznalaženju novih ideja i koncepata, novih proizvoda koje potencijalni kupci traže i generalno unapređuje korisničko iskustvo na vašem sajtu.", + "SiteSearchKeyword": "Ključna riječ (pretraživanje)", + "SiteSearchKeywordsDocumentation": "Ovaj izvještaj prikazuje ključne riječi koje su vaši posjetioci tražili u polju za pretragu.", + "SiteSearchKeywordsNoResultDocumentation": "Ovaj izvještaj prikazuje ključne reči koje nisu vratile nijedan rezultat pretrage. Možda algoritam pretrage može da se poboljša ili možda posjetioci traže sadržaje koji (još uvjek) nisu na sajtu?", + "SubmenuPageTitles": "Nazivi stranica", + "SubmenuPagesEntry": "Ulazne stranice", + "SubmenuPagesExit": "Izlazna stranica", + "SubmenuSitesearch": "Pretraživanje sajta", + "WidgetEntryPageTitles": "Nazivi ulaznih stranica", + "WidgetExitPageTitles": "Naslovi izlaznih stranica", + "WidgetPageTitles": "Naslovi stranica", + "WidgetPageTitlesFollowingSearch": "Naslovi stranica kod pretraživanja", + "WidgetPageUrlsFollowingSearch": "Stranice koje slijede poslije pretrage sajta", + "WidgetPagesEntry": "Ulazne stranice", + "WidgetPagesExit": "Izlazne stranice", + "WidgetSearchCategories": "Kategorije za pretrage", + "WidgetSearchKeywords": "Ključne riječi za pretragu na sajtu", + "WidgetSearchNoResultKeywords": "Ključne riječi za pretragu bez rezultata" + } +} diff --git a/plugins/Actions/lang/ca.json b/plugins/Actions/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..833e3effc36984ef4d1cb2d58df3ff849f8d6d87 --- /dev/null +++ b/plugins/Actions/lang/ca.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Tipus d'acció", + "ActionTypeSegmentHelp": "Un tipus d'acció, com ara: %s", + "ActionTypes": "Tipus d'acció", + "ActionsInVisit": "Accions a visites", + "AvgGenerationTimeTooltip": "Promig basat en %1$s accessos %2$s entre %3$s i %4$s", + "Behaviour": "Comportament", + "ColumnActionURL": "URL d'acció", + "ColumnClickedURL": "Clic fet a enllaç sortint", + "ColumnClickedURLs": "Clic fet a enllaços sortints", + "ColumnClicks": "Clics", + "ColumnClicksDocumentation": "Nombre de vegades que s'ha fet clic en aquest enllaç.", + "ColumnDownloadURL": "URL de descàrrega", + "ColumnDownloadURLs": "URLs de descàrrega", + "ColumnEntryPageTitle": "Títol de la pàgina d'entrada", + "ColumnEntryPageURL": "URL de la pàgina d'entrada", + "ColumnEntryPageURLs": "URLs de pàgines d'entrada", + "ColumnExitPageTitle": "Títol de la pàgina de sortida", + "ColumnExitPageURL": "URL de la pàgina de sortida", + "ColumnExitPageURLs": "URLs de pàgines de sortida", + "ColumnIdPageview": "ID de pàgina vista", + "ColumnInteractionPosition": "Posició d'interacció", + "ColumnNoResultKeyword": "Paraula clau sense cap resultat de cerca", + "ColumnPageName": "Nom de la pàgina", + "ColumnPageURL": "URL de la pàgina", + "ColumnPageViewPosition": "Posició de vista de pàgina", + "ColumnPagesPerSearch": "Pàgines dels resultats de la cerca", + "ColumnPagesPerSearchDocumentation": "Els visitants cerquen al vostre lloc web i, de vegades, fan clic a \"següent\" per veure més resultats. Aquest és el nombre mitjà de pàgines de resultats de cerca vistes per a aquesta paraula clau.", + "ColumnSearchCategory": "Categoria de cerca", + "ColumnSearchExits": "%% sortides de cerca", + "ColumnSearchExitsDocumentation": "El percentatge de visites que marxen del vostre lloc web desprès de cercar aquesta paraula al cercador del vostre lloc web.", + "ColumnSearchResultsCount": "Resultats de la cerca", + "ColumnSearches": "Cerques", + "ColumnSearchesDocumentation": "Nombre de visitants que han cercat aquesta paraula clau al cercador de la vostra pàgina web.", + "ColumnSiteSearchKeywords": "Paraules clau úniques", + "ColumnUniqueClicks": "Clics únics", + "ColumnUniqueClicksDocumentation": "El nombre de visites que han fet clic en aquest enllaç. Si s'ha fet clic en un enllaç diverses vegades durant una visita, només es compta una vegada.", + "ColumnUniqueDownloads": "Descàrregues úniques", + "ColumnUniqueOutlinks": "Enllaços de sortida únics", + "DownloadsReportDocumentation": "En aquest informe podeu observar quins fitxers han descarrregats els visitants. %s El Matomo només té constància dels clicks als enllaços de descarga, desconeix si la descàrrega s'ha completat o no.", + "DownloadsSubcategoryHelp1": "En aquest informe podeu veure quins fitxers han baixat els vostres visitants.", + "DownloadsSubcategoryHelp2": "El que Matomo compta com a descàrrega és fer clic a l'enllaç de descàrrega. Matomo no sap si la descàrrega es va completar o no.", + "EntryPageTitles": "Títols de les pàgines d'entrada", + "EntryPageTitlesReportDocumentation": "Aquest informe conté la informació sobre els títols de les pàgines d'entrada que s'han utilitzat durant el període especificat.", + "EntryPagesReportDocumentation": "Aquest informe conté informació sobre les pàgines d'entrada durant el període especificat. Una pàgina d'entrada és la primera pàgina que visualitza un usuari durant la seva visita. %s Les URL d'entrada es mostren com una estructura de carpetes.", + "EntryPagesSubcategoryHelp1": "Aquest informe conté informació sobre les pàgines d'entrada durant el període especificat. Una pàgina d'entrada és la primera pàgina que visualitza un usuari durant la seva visita.", + "EntryPagesSubcategoryHelp2": "Les URL d'entrada es mostren com una estructura de carpetes.", + "ExitPageTitles": "Títols de les pàgines de sortida", + "ExitPageTitlesReportDocumentation": "Aquest informe conté els títols de les pàgins de sortida que s'han utiltizat durant el període especificat.", + "ExitPagesReportDocumentation": "Aquest informe conté informació sobre les pàgines de sortida durant el període especificat. Una pàgina de sortida és l'última pàgina que visualitza un usuari durant la seva visita. %s Les URL de sortida es mostren com una estructura de carpetes.", + "ExitPagesSubcategoryHelp1": "Aquest informe conté informació sobre les pàgines de sortida durant el període especificat. Una pàgina de sortida és l'última pàgina que visualitza un usuari durant la seva visita.", + "ExitPagesSubcategoryHelp2": "Les URL de sortida es mostren com una estructura de carpetes.", + "MainMetricsReportDocumentation": "Aquest informe ofereix una visió general molt bàsica de quines accions fan els vostres visitants al vostre lloc web.", + "OneSearch": "1 cerca", + "OutlinkDocumentation": "Un enllaç extern és un enllaç que allunya el visitant del vostre lloc web (a un altre domini).", + "OutlinksReportDocumentation": "Aquest informe mostra una llista jeràrquica de les URL d'enllaços de sortida als quals han fet clic els vostres visitants.", + "OutlinksSubcategoryHelp1": "Aquest informe mostra una llista jeràrquica de les URL d'enllaços externs als quals han fet clic els vostres visitants. Un enllaç extern és un enllaç que allunya el visitant del vostre lloc web (a un altre domini).", + "PageTitlesReportDocumentation": "Aquest informe conté informació sobre els títols de les pàgines que s'han visitat. %1$s El títol de la pàgina es el tag HTML: %2$s, que es mostra al títol de la finestra en la majoría de navegadors.", + "PageTitlesSubcategoryHelp1": "Aquest informe conté informació sobre els títols de les pàgines que s'han visitat.", + "PageTitlesSubcategoryHelp2": "El títol de la pàgina és l'etiqueta HTML &lt;títol&gt; que la majoria dels navegadors mostren al títol de la finestra.", + "PageUrls": "URLs de les pàgines", + "PageViewsByVisitor": "Nombre de vegades que aquest visitant ha vist aquesta pàgina en totes les visites", + "PagesReportDocumentation": "Aquest informe conté les URL de les pàgines que s'han visitat. %s La taula s'organitza jerarquicament, i les URL es mostren amb estructura de directoris.", + "PagesSubcategoryHelp1": "Aquest informe conté informació sobre les URL de les pàgines visitades.", + "PagesSubcategoryHelp2": "La taula està organitzada jeràrquicament, les URL es mostren com una estructura de carpetes.", + "PagesSubcategoryHelp3": "Fes servir les icones de més i menys a l'esquerra per navegar.", + "PluginDescription": "Informes sobre les pàgines vistes i els títols de les pàgines. Permet mesurar el motor de cerca del teu lloc web intern. Fa un seguiment automàtic dels clics als enllaços externs i a les descàrregues de fitxers.", + "RevisitedPages": "Pàgines vistes més d'una vegada", + "SearchesSegmentHelp": "Per seleccionar totes les visites que han utilitzat la cerca interna Site Search, utilitzeu: %s", + "SiteSearchCategories": "Categories (cerca al lloc)", + "SiteSearchCategories1": "Aquest informe mostra les categories que els visitants van seleccionar quan van fer una cerca al vostre lloc web.", + "SiteSearchCategories2": "Per exemple, els llocs web de comerç electrònic solen tenir un selector de \"Categoria\" perquè els visitants puguin restringir les seves cerques a només els productes d'una categoria específica.", + "SiteSearchCategory": "Categoria (cerca al lloc)", + "SiteSearchFollowingPagesDoc": "Quan els visitants cerquen al teu lloc web, estan buscant una pàgina, un contingut, un producte o un servei en particular. Aquest informe mostra les pàgines que han estat clicades més vegades desprès d'una cerca interna. En altres paraules, la llista de pàgines que han estat més cercades pels visitants que ja están al vostre lloc web.", + "SiteSearchIntro": "Observar les cerques que els visitants fan al vostre lloc web és una forma molt efectiva d'aprendre més sobre el que està cercant la vostra audiència. Pot ajudar a trobar idees per a nou contingut, nous productes que els vostres visitants poden estar buscant i permet millorar l'experiència dels visitants al vostre lloc web.", + "SiteSearchKeyword": "Paraula clau (Cerca al lloc web)", + "SiteSearchKeywordCount": "Recompte de paraules clau (cerca al lloc)", + "SiteSearchKeywordCounts": "Recompte de paraules clau (cerca al lloc)", + "SiteSearchKeywords": "Paraules clau (cerca al lloc)", + "SiteSearchKeywordsDocumentation": "Aquest informe mostra les paraules clau que els visitants han utiltizat per cercar al cercador del vostre lloc web.", + "SiteSearchKeywordsNoResultDocumentation": "Aquest informe enumera les paraules clau de cerca que no han retornat cap resultat de la cerca: potser es pot millorar l'algoritme del motor de cerca, o potser els vostres visitants busquen contingut que no es troba (encara) al vostre lloc web?", + "SiteSearchSubcategoryHelp1": "La secció Cerca al lloc mostra quines paraules clau fan servir els visitants quan cerquen al vostre lloc web. També mostra quines pàgines veuen els usuaris després de fer una cerca i quines paraules clau de cerca al lloc no donen cap resultat.", + "SiteSearchSubcategoryHelp2": "Aquests informes us poden donar idees sobre el contingut que manca al vostre lloc, informació sobre què cerquen els vostres visitants però que no poden trobar fàcilment i molt més.", + "SiteSearchSubcategoryHelp3": "Més informació a la guia de cerca al lloc.", + "SubmenuPageTitles": "Títols de les pàgines", + "SubmenuPagesEntry": "Pàgines d'entrada", + "SubmenuPagesExit": "Pàgines de sortida", + "SubmenuSitesearch": "Cerca al lloc", + "TimeSpentInReferringAction": "Temps dedicat a l'acció de referència", + "WidgetEntryPageTitles": "Títols de les pàgines d'entrada", + "WidgetExitPageTitles": "Títols de les pàgines de sortida", + "WidgetPageTitles": "Títols de les pàgines", + "WidgetPageTitlesFollowingSearch": "Títols de pàgines després d'una cerca al lloc", + "WidgetPageUrlsFollowingSearch": "Pàgines després d'una cerca al lloc", + "WidgetPagesEntry": "Pàgines d'entrada", + "WidgetPagesExit": "Pàgines de sortida", + "WidgetSearchCategories": "Categories de cerca", + "WidgetSearchKeywords": "Paraules de cerca al lloc", + "WidgetSearchNoResultKeywords": "Paraules clau de cerca sense resultats" + } +} diff --git a/plugins/Actions/lang/cs.json b/plugins/Actions/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..be90a262f368c75f3b476e414ba01393c2e6a9b0 --- /dev/null +++ b/plugins/Actions/lang/cs.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Typ akce", + "ActionTypes": "Typy akcí", + "ActionsInVisit": "Akce během návštěvy", + "AvgGenerationTimeTooltip": "Průměr dle %1$s přístupů %2$s mezi %3$s a %4$s", + "Behaviour": "Chování", + "ColumnActionURL": "URL akce", + "ColumnClickedURL": "Kliknutý externí odkaz", + "ColumnClickedURLs": "Kliknuté externí odkazy", + "ColumnClicks": "Prokliků", + "ColumnClicksDocumentation": "Počet kliknutí na odkaz v závislosti na čase.", + "ColumnDownloadURL": "Stažení URL", + "ColumnDownloadURLs": "Stáhnout adresy URL", + "ColumnEntryPageTitle": "Titulek vstupní stránky", + "ColumnEntryPageURL": "URL vstupní stránky", + "ColumnEntryPageURLs": "URL vstupních stránek", + "ColumnExitPageTitle": "Titulek výstupní stránky", + "ColumnExitPageURL": "URL výstupní stránky", + "ColumnExitPageURLs": "URL výstupních stránek", + "ColumnIdPageview": "ID zobrazení stránky", + "ColumnInteractionPosition": "Pozice interakce", + "ColumnNoResultKeyword": "Hledané klíčové slovo nebylo nalezeno", + "ColumnPageName": "Jméno stránky", + "ColumnPageURL": "URL stránky", + "ColumnPageViewPosition": "Pozice zobrazení stránek", + "ColumnPagesPerSearch": "Stránky s výsledky vyhledávání", + "ColumnPagesPerSearchDocumentation": "Návštěvníci budou hledat na vašem webu a někdy kliknutím na tlačítko „další“ zobrazíte další výsledky. Toto je průměrný počet stránek s výsledky vyhledávání zobrazených pro toto klíčové slovo.", + "ColumnSearchCategory": "Hledání kategorie", + "ColumnSearchExits": "%% odchodů z vyhledávání", + "ColumnSearchExitsDocumentation": "Procento návštěv, které opustily stránku po nalezení tohoto klíčového slova pomocí vyhledávače.", + "ColumnSearchResultsCount": "Počet nalezených výsledků", + "ColumnSearches": "Vyhledávání", + "ColumnSearchesDocumentation": "Počet uživatelů, kteří hledali tato klíčová slova ve vyhledávači na Vašich stránkách.", + "ColumnSiteSearchKeywords": "Jedinečných klíčových slov", + "ColumnUniqueClicks": "Jedinečných prokliků", + "ColumnUniqueClicksDocumentation": "Počet návštěv, které klikly na tento odkaz. Pokud bylo na odkaz kliknuto vícekrát při jedné návštěvě, je kliknutí započítáno jen jednou.", + "ColumnUniqueDownloads": "Jedinečných stažení", + "ColumnUniqueOutlinks": "Jedinečné externí odkazy", + "DownloadsReportDocumentation": "V tomto reportu můžete vidět soubory, které byly staženy. %s Matomo počítá jako stažené jen ty, na které bylo kliknuto. Bez ohledu na to, jestli bylo stažení dokončeno nebo přerušeno.", + "DownloadsSubcategoryHelp1": "V tomto přehledu uvidíte, které soubory si vaši návštěvníci stáhli.", + "DownloadsSubcategoryHelp2": "To, co Matomo počítá jako stahování, je kliknutí na odkaz ke stažení. Zda bylo stahování dokončeno nebo ne, Matomo neví.", + "EntryPageTitles": "Titulky vstupních stránek", + "EntryPageTitlesReportDocumentation": "Toto hlášení obsahuje informace o titulcích vstupních stránek, které byly použity v daném období.", + "EntryPagesReportDocumentation": "Tento report obsahuje informace o vstupních stránkách, které byly za dané časové období použity. Vstupní stránka je první stránka, kterou uživatel navštíví. %s Vstupní URL jsou zobrazeny ve složkové struktuře.", + "EntryPagesSubcategoryHelp1": "Tato sestava obsahuje informace o vstupních stránkách, které byly použity během zadaného období. Vstupní stránka je první stránka, kterou si uživatel prohlíží během své návštěvy.", + "EntryPagesSubcategoryHelp2": "Vstupní adresy URL se zobrazují jako struktura složek.", + "ExitPageTitles": "Titulek výstupní stránky", + "ExitPageTitlesReportDocumentation": "Tento report obsahuje informace o titulcích výstupních stránek za dané období.", + "ExitPagesReportDocumentation": "Toto hlášení obsahujje informace o výstupních stránkách, které se za dané časové období vyskytly. Výstupní stránka je poslední stránka, kterou návštěvník během své návštěvy navštíví. %s Výstupní URL jsou zobrazeny ve složkové struktuře.", + "ExitPagesSubcategoryHelp1": "Tato sestava obsahuje informace o výstupních stránkách, které se objevily během zadaného období. Výstupní stránka je poslední stránka, kterou si uživatel prohlíží během své návštěvy.", + "ExitPagesSubcategoryHelp2": "Výstupní adresy URL se zobrazují jako struktura složek.", + "MainMetricsReportDocumentation": "Tento přehled poskytuje velmi základní přehled o tom, jaké akce návštěvníci na vašem webu provádějí.", + "OneSearch": "1 Vyhledávání", + "OutlinkDocumentation": "Externí odkaz je odkaz, který vede pryč z vašich stránek (na jinou doménu).", + "OutlinksReportDocumentation": "Toto hlášení zobrazuje hyerarchický seznam externích URL, na které vaši návštěvníci klikli.", + "OutlinksSubcategoryHelp1": "Tento přehled zobrazuje hierarchický seznam odchozích adres URL, na které klikli vaši návštěvníci. Externí link je odkaz, který vede návštěvníka pryč z vašeho webu (do jiné domény).", + "PageTitlesReportDocumentation": "Toto hlášení obsahuje informace o titulcích navštívených stránek. %1$s Titulek je HTML tag %2$s, který většina prohlížečů zobrazuje v titulku okna.", + "PageTitlesSubcategoryHelp1": "Tento report obsahuje informace o titulcích stránek, které byly navštíveny.", + "PageTitlesSubcategoryHelp2": "Titulek stránky je HTML &lt;title&gt; Štítek, který většina prohlížečů zobrazuje v názvu okna.", + "PageUrls": "URL stránky", + "PageViewsByVisitor": "Počet zobrazení této stránky návštěvníkem během všech návštěv", + "PagesReportDocumentation": "Tento report obsahuje informace o navštívených URL. %s Tabulka je organizována hyerarchicky jako strom složek.", + "PagesSubcategoryHelp1": "Tento report obsahuje informace o navštívených stránkách URL.", + "PagesSubcategoryHelp2": "Tabulka je uspořádána hierarchicky, adresy URL jsou zobrazeny jako struktura složek.", + "PagesSubcategoryHelp3": "Použijte ikonu plus a mínus vlevo k navigaci.", + "PluginDescription": "Hlášení o zobrazeních a názvech stránek. Umožňuje měřit interní vyhledávač vašich webových stránek. Automaticky sleduje kliknutí na externí odkazy a stahování souborů.", + "RevisitedPages": "Stránky navštívené více než jednou", + "SiteSearchCategories": "Kategorie (Vyhledávání na webu)", + "SiteSearchCategories1": "Tento report shrnuje kategorie, které návštěvníci vybrali při vyhledávání na stránkách.", + "SiteSearchCategories2": "Například elektronické obchody mívají filtr kategorií, aby mohli zákazníci zvolit, ve které kategorii bude provedeno hledání.", + "SiteSearchCategory": "Kategorie (Vyhledávání na webu)", + "SiteSearchFollowingPagesDoc": "Když návštěvníci hledají na vašich stránkách, snaží se nalézt určitou stránku, obsah, produkt, nebo službu. Toto hlášení zobrazuje stránky, na které bylo klikáno nejčastěji po interním vyhledávání. Jinak řečeno, jedná se o stránky, které byly nejvíce hledány návštěvníky, kteří už byli na vašich stránkách.", + "SiteSearchIntro": "Sledování vyhledávání na vašich stránkách je velmi efektivní způsob, jak se dozvědět více o tom, co vaši návštěvníci na stránkách hledají, jaký obsah by je mohl zajímat, jaké produkty by si mohli chtít koupit, a jak jim obecně zpříjemnit pobyt na vašich stránkách.", + "SiteSearchKeyword": "Klíčové slovo (Vyhledávání)", + "SiteSearchKeywordCount": "Počet klíčových slov (Vyhledávání)", + "SiteSearchKeywordCounts": "Počty klíčových slov (Vyhledávání)", + "SiteSearchKeywords": "Klíčová slova (vyhledávání na webu)", + "SiteSearchKeywordsDocumentation": "Tento report shrnuje klíčová slova, která byla použita návštěvníky při hledání pomocí interního vyhledávače.", + "SiteSearchKeywordsNoResultDocumentation": "Toto hlášení shrnuje klíčová slova, pro která nebyl vrácen žádný výsledek. Možná by bylo možné vylepšit vyhledávací algoritmus, nebo návštěvníci hledají obsah, který (zatím) není na vašich stránkách?", + "SiteSearchSubcategoryHelp1": "Sekce Vyhledávání stránek ukazuje, jaká klíčová slova návštěvníci používají při vyhledávání na vašem webu. Zobrazuje také, které stránky si uživatelé prohlížejí po provedení vyhledávání a která klíčová slova pro vyhledávání na webu nevracejí vůbec žádné výsledky.", + "SiteSearchSubcategoryHelp2": "Tyto přehledy vám mohou poskytnout představu o chybějícím obsahu na vašem webu, nahlédnout do toho, co vaši návštěvníci hledají a nemohou najít, atd.", + "SiteSearchSubcategoryHelp3": "Další informace naleznete v Site Search Guide.", + "SubmenuPageTitles": "Titulky stránek", + "SubmenuPagesEntry": "Vstupní stránky", + "SubmenuPagesExit": "Odchozí stránky", + "SubmenuSitesearch": "Vyhledat stránku", + "TimeSpentInReferringAction": "Čas strávený v odkazující akci", + "WidgetEntryPageTitles": "Titulky vstupní stránky", + "WidgetExitPageTitles": "Titulky výstupní stránky", + "WidgetPageTitles": "Titulky stránek", + "WidgetPageTitlesFollowingSearch": "Titulky stránky po hledání na stránce", + "WidgetPageUrlsFollowingSearch": "Stránky po hledání na stránce", + "WidgetPagesEntry": "Vstupní stránky", + "WidgetPagesExit": "Výstupní stránky", + "WidgetSearchCategories": "Vyhledávání kategorií", + "WidgetSearchKeywords": "Klíčová slova hledané stránky", + "WidgetSearchNoResultKeywords": "Hledaná Klíčová slova bez výsledku hledání" + } +} diff --git a/plugins/Actions/lang/cy.json b/plugins/Actions/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..9fa7e525126f330fa1522ff6ce33461738a6ed13 --- /dev/null +++ b/plugins/Actions/lang/cy.json @@ -0,0 +1,5 @@ +{ + "Actions": { + "PagesSubcategoryHelp3": "Defnyddiwch yr eiconau mwy (+) a llai (-) ar y chwith i lywio." + } +} diff --git a/plugins/Actions/lang/da.json b/plugins/Actions/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..7e1bac7e2f88ea9d9834d7883d14049f5669f6ef --- /dev/null +++ b/plugins/Actions/lang/da.json @@ -0,0 +1,79 @@ +{ + "Actions": { + "ActionType": "Handlingstype", + "ActionTypes": "Handlingstyper", + "ActionsInVisit": "Handlinger i Besøg", + "AvgGenerationTimeTooltip": "Gennemsnit baseret på %1$s hits %2$s mellem %3$s og %4$s", + "Behaviour": "Adfærd", + "ColumnActionURL": "Handlings-URL", + "ColumnClicks": "Klik", + "ColumnClicksDocumentation": "Antal gange linket blev klikket på.", + "ColumnDownloadURL": "Hentet URL", + "ColumnDownloadURLs": "Hent URLer", + "ColumnEntryPageTitle": "Indgangssidetitel", + "ColumnEntryPageURL": "Indgangsside-URL", + "ColumnEntryPageURLs": "Indgangsside-URL’er", + "ColumnExitPageTitle": "Udgangssidetitel", + "ColumnExitPageURL": "Udgangsside-URL", + "ColumnExitPageURLs": "Udgangsside-URL’er", + "ColumnIdPageview": "Sidevisning ID", + "ColumnInteractionPosition": "Position for Interaktion", + "ColumnNoResultKeyword": "Søgeord uden nogen resultater", + "ColumnPageName": "Sidenavn", + "ColumnPageURL": "Side-URL", + "ColumnPagesPerSearch": "Søgeresultatssider", + "ColumnPagesPerSearchDocumentation": "Besøgende vil søge på hjemmesiden, og vil nogle gange klikke på \"Næste\" for at se flere resultater. Dette er det gennemsnitlige antal søgeresultatsider set for dette søgeord.", + "ColumnSearchCategory": "Søgekategori", + "ColumnSearchExits": "%% søgeafslutninger", + "ColumnSearchExitsDocumentation": "Procentdelen af besøg, der forlod hjemmesiden efter en søgning på søgeordet i søgemaskinen.", + "ColumnSearchResultsCount": "Antal søgeresultater", + "ColumnSearches": "Søgninger", + "ColumnSearchesDocumentation": "Antal besøg, der søgte efter dette søgeord på hjemmesidens søgemaskine.", + "ColumnSiteSearchKeywords": "Unikke søgeord", + "ColumnUniqueClicks": "Unikke klik", + "ColumnUniqueClicksDocumentation": "Antal besøg, der involverede et klik på linket. Hvis et link blev klikket flere gange under et besøg, tæller det kun én gang.", + "ColumnUniqueDownloads": "Unikke downloads", + "ColumnUniqueOutlinks": "Unikke udgående links", + "DownloadsReportDocumentation": "Rapporten viser, hvilke filer dine besøgende har hentet. %s Hvad Matomo tæller som en filhentning, er klikket på et link. Hvorvidt overførelsen er afsluttet eller ej ved Matomo ikke.", + "EntryPageTitles": "Indgangssidetitler", + "EntryPageTitlesReportDocumentation": "Rapporten indeholder oplysninger om titlerne på indgangssider, der blev brugt i den angivne periode.", + "EntryPagesReportDocumentation": "Rapporten indeholder oplysninger om indgangssider, der blev anvendt i den angivne periode. En indgangsside er den første side, en bruger får vist under sit besøg. %s Indgangssiden vises med en mappestruktur.", + "ExitPageTitles": "Udgangssidetitler", + "ExitPageTitlesReportDocumentation": "Rapporten indeholder oplysninger om titlerne på udgangssider, der blev brugt i den angivne periode.", + "ExitPagesReportDocumentation": "Rapporten indeholder oplysninger om de udgangssider, der blev brugt i den angivne periode. En udgangsside er den sidste side, en bruger får vist under sit besøg. %s udgangs-URLer vises med en mappestruktur.", + "OneSearch": "1 søgning", + "OutlinkDocumentation": "Et udgående link er et link, der fører den besøgende væk fra hjemmesiden (til et andet domæne)", + "OutlinksReportDocumentation": "Rapporten viser en hierarkisk liste over udgående netadresser, der blev klikket på af de besøgende", + "PageTitlesReportDocumentation": "Rapporten indeholder oplysninger om titlerne på de sider, der er blevet besøgt. %1$s side titel er HTML %2$s mærket, som de fleste netlæsere viser i vinduetitlen.", + "PageUrls": "Side-URL'er", + "PageViewsByVisitor": "Antal gange den besøgende har set denne side henover alle besøg", + "PagesReportDocumentation": "Rapporten indeholder oplysninger om sidens netadresser, der er blevet besøgt. %s tabellen er arrangeret hierarkisk, netadresserne vises i en mappestruktur.", + "PagesSubcategoryHelp3": "Brug plus og minus ikonerne til venstre til at navigere.", + "PluginDescription": "Rapporter om sidevisninger og sidetitler. Gør det muligt at måle det interne websted søgemaskine. Sporer automatisk klik på eksterne links og filhentninger.", + "RevisitedPages": "Sider vist mere end én gang", + "SiteSearchCategories": "Kategorier (Sitesøgning)", + "SiteSearchCategories1": "Rapporten viser de kategorier, som de besøgende valgte, da de lavede en søgning på hjemmesiden.", + "SiteSearchCategories2": "For eksempel har E-handel hjemmesider typisk en \"Kategori\" vælger, så besøgende kan begrænse deres søgninger til alle produkter i en bestemt kategori.", + "SiteSearchCategory": "Kategori (Sitesøgning)", + "SiteSearchFollowingPagesDoc": "Når besøgende søger på hjemmesiden, er de på udkig efter en bestemt side, indhold, produkt eller service. Rapporten viser de sider, der blev klikket mest på efter en intern søgning. Med andre ord, listen over sider mest søgt af besøgende allerede på hjemmesiden.", + "SiteSearchIntro": "Sporing af søgninger, som de besøgende foretager på hjemmesiden er en meget effektiv måde at lære mere om, hvad din målgruppe er på udkig efter, det kan hjælpe med til at finde idéer til nyt indhold, nye E-handel produkter, som potentielle kunder kan søge efter, og generelt forbedre de besøgendes oplever på hjemmesiden.", + "SiteSearchKeyword": "Søgeord (intern søgning)", + "SiteSearchKeywords": "Nøgleord (Sitesøgning)", + "SiteSearchKeywordsDocumentation": "Rapporten viser de søgeord, de besøgende søgte efter på den interne søgemaskine.", + "SiteSearchKeywordsNoResultDocumentation": "Rapporten viser de søgeord, der ikke returnerede noget søgeresultat: måske søgemaskinen algoritme kan forbedres, eller måske de besøgende er på udkig efter indhold, der (endnu) ikke er på din hjemmeside?", + "SubmenuPageTitles": "Sidetitler", + "SubmenuPagesEntry": "Indgangssider", + "SubmenuPagesExit": "Udgangssider", + "SubmenuSitesearch": "Webstedssøgning", + "WidgetEntryPageTitles": "Indgangssidetitler", + "WidgetExitPageTitles": "Udgangssidetitler", + "WidgetPageTitles": "Sidetitler", + "WidgetPageTitlesFollowingSearch": "Sidetitler efter en webstedssøgning", + "WidgetPageUrlsFollowingSearch": "Sider efter en webstedssøgning", + "WidgetPagesEntry": "Indgangssider", + "WidgetPagesExit": "Udgangssider", + "WidgetSearchCategories": "Søge kategorier", + "WidgetSearchKeywords": "Websted søgeord", + "WidgetSearchNoResultKeywords": "Søgeord uden nogen resultater" + } +} diff --git a/plugins/Actions/lang/de.json b/plugins/Actions/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..adfc6e8d0206db1a77f650274917bd664dace86e --- /dev/null +++ b/plugins/Actions/lang/de.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Typ der Aktion", + "ActionTypes": "Aktionstypen", + "ActionsInVisit": "Aktionen beim Besuch", + "AvgGenerationTimeTooltip": "Durchschnitt basierend auf %1$s Aufruf(en) %2$s zwischen %3$s und %4$s", + "Behaviour": "Verhalten", + "ColumnActionURL": "Aktions-URL", + "ColumnClickedURL": "Gewählter ausgehender Verweis", + "ColumnClickedURLs": "Gewählte ausgehende Verweise", + "ColumnClicks": "Klicks", + "ColumnClicksDocumentation": "Anzahl der Klicks auf diesen Link.", + "ColumnDownloadURL": "Download-URL", + "ColumnDownloadURLs": "Download-URLs", + "ColumnEntryPageTitle": "Titel der Einstiegsseite", + "ColumnEntryPageURL": "URL der Einstiegsseite", + "ColumnEntryPageURLs": "Einstiegsseiten-URLs", + "ColumnExitPageTitle": "Titel der Ausstiegsseite", + "ColumnExitPageURL": "URL der Ausstiegsseite", + "ColumnExitPageURLs": "Ausgangsseiten URLs", + "ColumnIdPageview": "Seitenansicht-ID", + "ColumnInteractionPosition": "Interaktionsposition", + "ColumnNoResultKeyword": "Suchbegriff ohne Ergebnisse", + "ColumnPageName": "Seitenname", + "ColumnPageURL": "Seiten-URL", + "ColumnPageViewPosition": "Seitenansichtposition", + "ColumnPagesPerSearch": "Besuchte Ergebnisseiten", + "ColumnPagesPerSearchDocumentation": "In dieser Spalte sehen Sie, wie oft die Ergebnisseiten des Suchbegriffs im Durchschnitt aufgerufen wurden. Die Ergebnisse werden beispielsweise mehrere Male aufgerufen, wenn sie in mehrere Seiten aufgespalten sind und der Besucher verschiedene Seiten aufruft.", + "ColumnSearchCategory": "Suchkategorie", + "ColumnSearchExits": "%% Ausstiege nach Suche", + "ColumnSearchExitsDocumentation": "Der Anteil der Besuche, die nach der internen Suche nach dem Suchbegriff die Webseite verlassen haben.", + "ColumnSearchResultsCount": "Anzahl Suchergebnisse", + "ColumnSearches": "Suchanfragen", + "ColumnSearchesDocumentation": "Die Anzahl der Besucher, die nach diesem Begriff in der internen Suche gesucht haben.", + "ColumnSiteSearchKeywords": "Eindeutige Suchbegriffe", + "ColumnUniqueClicks": "Eindeutige Klicks", + "ColumnUniqueClicksDocumentation": "Die Anzahl der Besuche, bei denen dieser Link angeklickt wurde. Falls der Link mehrmals pro Besuch angeklickt wurde, wird er nur einmal gezählt.", + "ColumnUniqueDownloads": "Eindeutige Downloads", + "ColumnUniqueOutlinks": "Eindeutige ausgehende Verweise", + "DownloadsReportDocumentation": "In diesem Bericht sehen Sie, welche Dateien Ihre Besucher heruntergeladen haben. %s Als Download wird in Matomo der Klick auf einen Downloadlink gewertet. Ob der Download abgeschlossen wurde oder nicht, ist Matomo nicht bekannt.", + "DownloadsSubcategoryHelp1": "In diesem Bericht können Sie sehen, welche Dateien Ihre Besucher heruntergeladen haben.", + "DownloadsSubcategoryHelp2": "Als Download zählt bei Matomo der Klick auf einen Download-Link. Ob der Download abgeschlossen wurde oder nicht, ist Matomo nicht bekannt.", + "EntryPageTitles": "Titel der Einstiegsseite", + "EntryPageTitlesReportDocumentation": "Dieser Bericht enthält Informationen über die Titel der Einstiegsseiten, die im angeforderten Zeitraum verwendet wurden.", + "EntryPagesReportDocumentation": "Dieser Bericht enthält Informationen über die Einstiegsseiten im gewählten Zeitraum. Die Einstiegsseite ist die erste Seite, die während eines Besuches aufgerufen wird. %s Die URLs werden als Ordnerstruktur angezeigt.", + "EntryPagesSubcategoryHelp1": "Dieser Bericht enthält Informationen über die Einstiegsseiten im gewählten Zeitraum. Die Einstiegsseite ist die erste Seite, die während eines Besuches aufgerufen wird.", + "EntryPagesSubcategoryHelp2": "Die Einstiegs-URLs werden als Verzeichnisstruktur angezeigt.", + "ExitPageTitles": "Titel der Ausstiegsseite", + "ExitPageTitlesReportDocumentation": "Dieser Bericht enthält Informationen über die Titel der Ausstiegsseiten, die angeforderten Zeitraum verwendet wurden.", + "ExitPagesReportDocumentation": "Dieser Bericht enthält Informationen über die Ausstiegsseiten im gewählten Zeitraum. Die Ausstiegsseite ist die letzte Seite, die während eines Besuches aufgerufen wird. %s Die URLs werden als Ordnerstruktur angezeigt.", + "ExitPagesSubcategoryHelp1": "Dieser Bericht enthält Informationen über die Ausstiegsseiten im gewählten Zeitraum. Die Ausstiegsseite ist die letzte Seite, die während eines Besuches aufgerufen wird.", + "ExitPagesSubcategoryHelp2": "Die Ausstiegs-URLs werden als Verzeichnisstruktur angezeigt.", + "MainMetricsReportDocumentation": "Dieser Bericht stellt einen sehr groben Überblick zur Verfügung darüber, welche Aktionen Ihre Besucher auf Ihrer Webseite ausgeführt haben.", + "OneSearch": "1 Suche", + "OutlinkDocumentation": "Ein ausgehender Verweis ist ein Link der einen Besucher auf eine andere Webseite führt (auf eine andere Domain).", + "OutlinksReportDocumentation": "Dieser Bericht zeigt eine hierarchisch angeordnete Liste von ausgehenden Links, die von ihren Besuchern angeklickt wurden.", + "OutlinksSubcategoryHelp1": "Dieser Bericht zeigt eine hierarchisch angeordnete Liste von ausgehenden Links, die von ihren Besuchern angeklickt wurden. Ein ausgehender Link ist ein Link, der den Besucher von Ihrer Webseite wegführt (zu einer anderen Domain).", + "PageTitlesReportDocumentation": "Dieser Bericht enthält Informationen über die Seitentitel der besuchten Seiten. %1$s Der Seitentitel ist der HTML %2$s Tag, der von den meisten Browsern in ihrer Titelleiste angezeigt wird.", + "PageTitlesSubcategoryHelp1": "Dieser Bericht enthält Informationen über die Titel der Seiten, welche besucht wurden.", + "PageTitlesSubcategoryHelp2": "Der Seitentitel ist der HTML-&lt;title&gt; Tag, welcher von den meisten Browsern in deren Fenstertitel angezeigt wird.", + "PageUrls": "Seiten URL", + "PageViewsByVisitor": "Die Anzahl, wie häufig diese Seite von diesem Besucher insgesamt besucht wurde", + "PagesReportDocumentation": "Dieser Bericht enthält Informationen über die URLs der besuchten Seiten. %s Die Tabelle ist hierarchisch strukturiert, die URLs werden als Ordnerstruktur angezeigt.", + "PagesSubcategoryHelp1": "Dieser Bericht enthält Informationen über die Seiten URLs, welche besucht wurden.", + "PagesSubcategoryHelp2": "Die Tabelle ist hierarchisch organisiert, die URLs werden als Verzeichnisstruktur angezeigt.", + "PagesSubcategoryHelp3": "Benutzen Sie zur Navigation die Symbole Plus und Minus auf der linken Seite.", + "PluginDescription": "Berichte über die Seitenansichten und Seitentitel. Lässt Sie die interne Suchmaschine Ihrer Webseite bewerten. Registriert Klicks auf externe Links und Dateidownloads automatisch.", + "RevisitedPages": "Seiten die mehrmals angesehen wurden", + "SiteSearchCategories": "Kategorien (Interne Suche)", + "SiteSearchCategories1": "Dieser Bericht zeigt die Kategorien, welche bei der internen Suche ausgewählt wurden.", + "SiteSearchCategories2": "Onlineshops haben beispielsweise meist eine Kategorieauswahl, mit der Besucher ihre Suche auf eine bestimmte Produktkategorie einschränken können.", + "SiteSearchCategory": "Kategorie (Interne Suche)", + "SiteSearchFollowingPagesDoc": "Wenn Besucher die interne Suche verwenden, suchen sie meist nach einer bestimmten Seite, bzw. einem bestimmen Inhalt. In diesem Bericht wird angezeigt, welche Seiten am häufigsten von den Suchergebnissen aus besucht wurden. In anderen Worten sind dies die Seiten, nach denen Besucher am häufigsten gesucht haben, nachdem sie sich bereits auf der Webseite befanden.", + "SiteSearchIntro": "Die Suchbegriffe der internen Suche auszuwerten, hilft dabei zu verstehen, was Ihre Zielgruppe sucht. So können Ideen für neue Inhalte oder Produkte entstehen und bestehende Inhalte für die Sprache der Besucher optimiert werden.", + "SiteSearchKeyword": "Suchbegriff (interne Suche)", + "SiteSearchKeywordCount": "Anzahl Suchbegriffe (interne Suche)", + "SiteSearchKeywordCounts": "Gezählte Suchbegriffe (interne Suche)", + "SiteSearchKeywords": "Suchbegriffe (Interne Suche)", + "SiteSearchKeywordsDocumentation": "Dieser Bericht enthält die Suchbegriffe, nach denen Besucher in der internen Suche gesucht haben.", + "SiteSearchKeywordsNoResultDocumentation": "Dieser Bericht zeigt die Suchbegriffe an, welche keine Ergebnisse hatten. Vielleicht könnten diese Begriffe in bestehende Seiten eingebaut werden, um es den Besuchern zu erleichtern, die Inhalte zu finden?", + "SiteSearchSubcategoryHelp1": "Der Bereich \"Interne Suche\" zeigt, welche Suchworte Besucher verwenden, wenn sie auf Ihrer Webseite suchen. Er zeigt auch auf welche Seiten Benutzer gehen, nachdem sie gesucht haben und welche Suchbegriffe gar keine Resultate erzeugt hatten.", + "SiteSearchSubcategoryHelp2": "Dieser Bericht kann Ihnen Hinweise darauf geben, welche Inhalte auf Ihrer Seite eventuell fehlen, in dem er aufzeigt, was Ihre Besucher suchen, aber nicht so einfach finden.", + "SiteSearchSubcategoryHelp3": "Weitere Informationen finden Sie in der Anleitung zur internen Suche.", + "SubmenuPageTitles": "Seitentitel", + "SubmenuPagesEntry": "Einstiegsseiten", + "SubmenuPagesExit": "Ausstiegsseiten", + "SubmenuSitesearch": "Interne Suche", + "TimeSpentInReferringAction": "Verwendete Zeit für Verweisaktionen", + "WidgetEntryPageTitles": "Titel der Einstiegsseiten", + "WidgetExitPageTitles": "Titel der Ausgangsseiten", + "WidgetPageTitles": "Seitentitel", + "WidgetPageTitlesFollowingSearch": "Seitentitel nach einer internen Suche", + "WidgetPageUrlsFollowingSearch": "Besuchte Seiten nach einer internen Suche", + "WidgetPagesEntry": "Einstiegsseiten", + "WidgetPagesExit": "Ausstiegsseiten", + "WidgetSearchCategories": "Suchkategorien", + "WidgetSearchKeywords": "Suchbegriffe (interne Suche)", + "WidgetSearchNoResultKeywords": "Suchbegriffe ohne Ergebnisse" + } +} diff --git a/plugins/Actions/lang/dv.json b/plugins/Actions/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..4eb6f38dd33d5aba00e7934140de3bf330259091 --- /dev/null +++ b/plugins/Actions/lang/dv.json @@ -0,0 +1,14 @@ +{ + "Actions": { + "ColumnClicks": "ކްލިކްތައް", + "ColumnClicksDocumentation": "މި ލިންކަށް ކްލިކް ކުރެވުނު އަދަދު", + "ColumnDownloadURL": "ޑައުންލޯޑް ޔޫއާރްއެލް", + "ColumnEntryPageTitle": "ވަން ސަފުހާގެ ސުރުހީ", + "ColumnEntryPageURL": "ވަން ސަފުހާގެ ޔޫއާރްއެލް", + "ColumnExitPageTitle": "ބޭރުވި ސަފުހާގެ ސުރުހީ", + "ColumnExitPageURL": "ބޭރުވި ސަފުހާގެ ޔޫއާރްއެލް", + "ColumnNoResultKeyword": "ނަތީޖާއެއް ހޯދިފައިނުވާ ބަސްތައް", + "ColumnPageName": "ސަފުހާގެ ނަން", + "ColumnPagesPerSearch": "ނަތީޖާތައް ހޯދާ ސަފުހާތައް" + } +} diff --git a/plugins/Actions/lang/el.json b/plugins/Actions/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..92fc6517c7b069d9def7268aecbdfc95381b0dc2 --- /dev/null +++ b/plugins/Actions/lang/el.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Τύπος Ενέργειας", + "ActionTypeSegmentHelp": "Ένας τύπος ενέργειας, όπως: %s", + "ActionTypes": "Τύποι ενεργειών", + "ActionsInVisit": "Δραστηριότητες στην επίσκεψη", + "AvgGenerationTimeTooltip": "Μέσος όρος βάσει %1$s επισκέψεων %2$s μεταξύ %3$s και %4$s", + "Behaviour": "Συμπεριφορά", + "ColumnActionURL": "Διεύθυνση URL ενέργειας", + "ColumnClickedURL": "Εξωτερικός σύνδεσμος με κλικ", + "ColumnClickedURLs": "Εξωτερικοί σύνδεσμοι με κλικ", + "ColumnClicks": "Κλικ", + "ColumnClicksDocumentation": "Οι φορές που πατήθηκε αυτός ο σύνδεσμος.", + "ColumnDownloadURL": "URL λήψης", + "ColumnDownloadURLs": "URL για μεταφορτώσεις", + "ColumnEntryPageTitle": "Τίτλος Σελίδας Εισαγωγής", + "ColumnEntryPageURL": "URL Σελίδας Εισαγωγής", + "ColumnEntryPageURLs": "URL σελίδων εισόδου", + "ColumnExitPageTitle": "Τίτλος Σελίδας Εξόδου", + "ColumnExitPageURL": "URL Σελίδας Εξόδου", + "ColumnExitPageURLs": "URL Σελίδων Εξόδου", + "ColumnIdPageview": "Αναγνωριστικό προβολής σελίδας", + "ColumnInteractionPosition": "Θέση Αλληλεπίδρασης", + "ColumnNoResultKeyword": "Λέξη-κλειδί Χωρίς Αποτέλεσμα Αναζήτησης", + "ColumnPageName": "Ονομασία Σελίδας", + "ColumnPageURL": "URL σελίδας", + "ColumnPageViewPosition": "Θέση Προβολής Σελίδων", + "ColumnPagesPerSearch": "Σελίδες Αποτελεσμάτων Αναζήτησης", + "ColumnPagesPerSearchDocumentation": "Οι επισκέπτες θα αναζητήσουν στον ιστοτόπο σας και μερικές φορές θα πατήσουν «επόμενο» για να δουν περισσότερα αποτελέσματα. Αυτός είναι ο μέσος αριθμός των σελίδων αποτελεσμάτων αναζήτησης που προβάλλονται για αυτή τη λέξη-κλειδί.", + "ColumnSearchCategory": "Κατηγορία Αναζήτησης", + "ColumnSearchExits": "%% έξοδοι από αναζητήσεις", + "ColumnSearchExitsDocumentation": "Το ποσοστό των επισκέψεων που άφησαν τον ιστοτόπο μετά την αναζήτηση για αυτή τη λέξη-κλειδί στην μηχανή Αναζήτησης του Ιστοτόπου σας.", + "ColumnSearchResultsCount": "Μετρητής Αποτελεσμάτων Αναζήτησης", + "ColumnSearches": "Αναζητήσεις", + "ColumnSearchesDocumentation": "Ο αριθμός των επισκέψεων που αναζήτησαν για αυτή την λέξη-κλειδί στη μηχανή αναζήτησης του ιστοτόπου σας.", + "ColumnSiteSearchKeywords": "Μοναδικές Λέξεις-κλειδιά", + "ColumnUniqueClicks": "Μοναδικά κλικ", + "ColumnUniqueClicksDocumentation": "Ο αριθμός των επισκέψεων που περιλαμβάνουν ένα πάτημα σε αυτό το σύνδεσμο. Αν ο σύνδεσμος πατήθηκε πολλές φορές σε μια επίσκεψη, μετριέται μόνο μια φορά.", + "ColumnUniqueDownloads": "Μοναδικές λήψεις", + "ColumnUniqueOutlinks": "Μοναδικοί Εξωτερικοί σύνδεσμοι", + "DownloadsReportDocumentation": "Σε αυτή την αναφορά, μπορείτε να δείτε ποια αρχεία κατέβασαν οι επισκέπτες σας. %s Αυτό που μετράει το Matomo ως λήψη είναι το πάτημα σε σύνδεσμο λήψης. Αν η λήψη ολοκληρώθηκε ή όχι δεν είναι γνωστό στο Matomo.", + "DownloadsSubcategoryHelp1": "Σε αυτή την αναφορά, μπορείτε να δείτε ποια αρχεία κατέβασαν οι επισκέπτες σας.", + "DownloadsSubcategoryHelp2": "Αυτό που μετράει το Matomo ως λήψη είναι το πάτημα σε σύνδεσμο λήψης. Αν η λήψη ολοκληρώθηκε ή όχι δεν είναι γνωστό στο Matomo.", + "EntryPageTitles": "Τίτλοι σελίδας εισόδου", + "EntryPageTitlesReportDocumentation": "Αυτή η αναφορά περιέχει πληροφορίες για τους τίτλους των σελίδων εισόδου που χρησιμοποιήθηκαν κατά την ορισμένη περίοδο.", + "EntryPagesReportDocumentation": "Η αναφορά περιέχει πληροφορίες για τις σελίδες εισόδου για συγκεκριμένη χρονική περίοδο. Μια σελίδα εισόδου είναι η πρώτη σελίδα που βλέπει ο χρήστης κατά την επίσκεψή του. %s Παρακάτω εμφανίζονται με δομή φακέλων οι διευθύνσεις URL εισόδου.", + "EntryPagesSubcategoryHelp1": "Η αναφορά περιέχει πληροφορίες σχετικά με τις σελίδες εισόδου που χρησιμοποιήθηκαν σε μια συγκεκριμένη χρονική περίοδο. Μια σελίδα εισόδου είναι η πρώτη σελίδα που βλέπει ένας χρήστης κατά την επίσκεψή του.", + "EntryPagesSubcategoryHelp2": "Οι διευθύνσεις URL εισόδου εμφανίζονται ως δομή φακέλων.", + "ExitPageTitles": "Τίτλοι σελίδας εξόδου", + "ExitPageTitlesReportDocumentation": "Αυτή η αναφορά περιέχει πληφορορίες για τους τίτλους των σελίδων εξόδου που συνέβησαν κατά την ορισμένη περίοδο.", + "ExitPagesReportDocumentation": "Η αναφορά περιέχει πληροφορίες για τις σελίδες εξόδου σε συγκεκριμένη χρονική περίοδο. Μια σελίδα εξόδου είναι η τελευταία σελίδα που βλέπει ένας χρήστης κατά την επίσκεψή του. %s Παρακάτω εμφανίζονται με δομή φακέλων οι διευθύνσεις URL εξόδου.", + "ExitPagesSubcategoryHelp1": "Η αναφορά περιέχει πληροφορίες για τις σελίδες εξόδου σε συγκεκριμένη χρονική περίοδο. Μια σελίδα εξόδου είναι η τελευταία σελίδα που βλέπει ένας χρήστης κατά την επίσκεψή του.", + "ExitPagesSubcategoryHelp2": "Οι διευθύνσεις URL εξόδου εμφανίζονται ως δομή φακέλων.", + "MainMetricsReportDocumentation": "Αυτή η αναφορά δείχνει μια βασική σύνοψη των ενεργειών που οι επισκέπτες σας προέβησαν στον ιστοτόπο σας.", + "OneSearch": "1 αναζήτηση", + "OutlinkDocumentation": "Ένας εξωτερικός σύνδεσμος είναι ένας σύνδεσμος που οδηγεί τον επισκέπτη εκτός της ιστοσελίδας (σε άλλη).", + "OutlinksReportDocumentation": "Αυτή η αναφορά δείχνει μια ιεραρχημένη λίστα με εξωτερικούς συνδέσμους διευθύνσεων ιστοσελίδων που πατήθηκαν από τους επισκέπτες σας.", + "OutlinksSubcategoryHelp1": "Η αναφορά δείχνει μια ιεραρχική λίστα με εξωτερικούς συνδέσμους διευθύνσεων ιστοσελίδων που πάτησαν οι επισκέπτες σας. Ένας εξωτερικός σύνδεσμος είναι ένας σύνδεσμος που οδηγεί τον επισκέπτη εκτός του ιστοτόπου (σε άλλο όνομα χώρου).", + "PageTitlesReportDocumentation": "Η αναφορά αυτή περιέχει πληροφορίες σχετικά με τους τίτλους των σελίδων που επισκέφθηκαν οι χρήστες. %1$s Ο τίτλος της σελίδας είναι η σήμανση HTML %2$s που τα περισσότερα προγράμματα πλοήγησης δείχνουν στον τίτλο του παραθύρου τους.", + "PageTitlesSubcategoryHelp1": "Αυτή η αναφορά περιέχει πληροφορίες για τους τίτλους των σελίδων που επισκέφτηκαν οι χρήστες.", + "PageTitlesSubcategoryHelp2": "Η σελίδα τίτλου είναι η σήμανση HTML &lt;title&gt; που οι περισσότεροι φυλλομετρητές εμφανίζουν στον τίτλο του παραθύρου τους.", + "PageUrls": "Διευθύνσεις σελίδων", + "PageViewsByVisitor": "Ο αριθμός των αναγνώσεων της σελίδας από αυτόν τον επισκέπτη σε όλες του τις επισκέψεις", + "PagesReportDocumentation": "Αυτή η αναφορά περιέχει πληροφορίες σχετικά με τις επισκεφθείσες διευθύνσεις σελίδων. %s Ο πίνακας ταξινομείται ιεραρχικά, οι διευθύνσεις σελίδων εμφανίζονται με δομή φακέλου.", + "PagesSubcategoryHelp1": "Αυτή η αναφορά περιέχει πληροφορίες για τις διευθύνσεις των σελίδων που επισκέφτηκαν οι χρήστες.", + "PagesSubcategoryHelp2": "Ο πίνακας είναι ιεραρχικά οργανωμένος, οι διευθύνσεις URL εμφανίζονται ως δομή φακέλων.", + "PagesSubcategoryHelp3": "Χρησιμοποιήστε τα εικονίδια + και - στα αριστερά για πλοήγηση.", + "PluginDescription": "Αναφέρει σχετικά με τις αναγνώσεις σελίδων και τους τίτλους τους. Επιτρέπει την μέτρηση της απόδοσης της εσωτερικής μηχανής αναζήτησης του ιστοτόπου σας. Αυτόματα παρακολουθεί τα κλικ προς εξωτερικούς συνδέσμους και μεταφορτώσεις αρχείων.", + "RevisitedPages": "Σελίδες που αναγνώστηκαν παραπάνω από μία φορές", + "SearchesSegmentHelp": "Για να επιλέξετε όλες τις επισκέψεις που χρησιμοποίησαν την Αναζήτηση Ιστοτόπου, χρησιμοποιήστε: %s", + "SiteSearchCategories": "Κατηγορίες (Αναζήτηση ιστοτόπου)", + "SiteSearchCategories1": "Αυτή η αναφορά δημιουργεί λίστα με τις Κατηγορίες που οι επισκέπτες επέλεξαν όταν έκαναν μια Αναζήτηση στην ιστοσελίδα σας.", + "SiteSearchCategories2": "Για παράδειγμα, οι ιστοσελίδες Ηλεκτρονικού Εμπορίου τυπικά έχουν έναν επιλογέα «Κατηγορίας» ώστε οι επισκέπτες μπορούν να περιορίσουν τις αναζητήσεις τους από όλα τα προϊόντα σε μια συγκεκριμένη κατηγορία.", + "SiteSearchCategory": "Κατηγορία (Αναζήτηση ιστοτόπου)", + "SiteSearchFollowingPagesDoc": "Όταν οι επισκέπτες αναζητούν στην ιστοσελίδα σας, αναζητούν μια συγκεκριμένη σελίδα, περιεχόμενο, προϊόν ή υπηρεσία. Αυτή η αναφορά δημιουργεί μια λίστα με τις σελίδες που επιλέχθηκαν περισσότερο μετά από μια εσωτερική αναζήτηση. Με άλλα λόγια, η λίστα των σελίδων που αναζητήθηκαν περίσσότερο από τους επισκέπτες που ήταν ήδη στην ιστοσελίδα σας.", + "SiteSearchIntro": "Η παρακολούθηση των αναζητήσεων που κάνουν οι επισκέπτες στην ιστοσελίδα σας είναι ένας πολύ αποτελεσματικός τρόπος για να μάθετε περισσότερα για το τί ενδιαφέρει το κοινό σας, μπορεί να σας βοηθήσει να βρείτε ιδέες για νέο περιεχόμενο, νέα προϊόντα ηλεκτρονικού εμπορίου για τα οποία οι πιθανοί πελάτες θα μπορούσαν να ψάχνουν και γενικά τη βελτίωση της εμπειρίας των επισκεπτών της ιστοσελίδας σας.", + "SiteSearchKeyword": "Λέξη κλειδί (Αναζήτηση ιστοτόπου)", + "SiteSearchKeywordCount": "Εμφάνιση λέξης κλειδιού (Αναζήτηση ιστοτόπου)", + "SiteSearchKeywordCounts": "Εμφανίσεις λέξεων κλειδιών (Αναζήτηση ιστοτόπου)", + "SiteSearchKeywords": "Λέξεις κλειδιά (Αναζήτηση ιστοτόπου)", + "SiteSearchKeywordsDocumentation": "Η παρούσα αναφορά απαριθμεί τις λέξεις-κλειδιά αναζήτησης που έψαξαν οι επισκέπτες στην εσωτερική μηχανή αναζήτησης σας.", + "SiteSearchKeywordsNoResultDocumentation": "Η παρούσα αναφορά παραθέτει τις λέξεις-κλειδιά αναζήτησης που δεν επιστρέψαν κανένα αποτέλεσμα αναζήτησης: ίσως ο αλγόριθμος της μηχανής αναζήτησης μπορεί να βελτιωθεί ή ίσως οι επισκέπτες σας ψάχνουν για περιεχόμενο που δεν είναι (ακόμη) στην ιστοσελίδα σας;", + "SiteSearchSubcategoryHelp1": "Το τμήμα Αναζήτηση Ιστοτόπου εμφανίζει τις λέξεις κλειδιά που οι επισκέπτες χρησιμοποίησαν κατά την αναζήτησή τους στον ιστοτόπο σας. Εμφανίζει επίσης τις σελίδες που βλέπουν οι χρήστες μετά την εκτέλεση της αναζήτησης και ποιες λέξεις κλειδιά δεν επιστρέφουν αποτελέσματα.", + "SiteSearchSubcategoryHelp2": "Οι αναφορές αυτές σας δίνουν ιδέες σχετικά με περιεχόμενο που μπορεί να λείπει από τον ιστοτόπο σας, εικόνα γύρω από αυτά που ψάχνουν οι επισκέπτες σας αλλά δεν βρήκαν εύκολα και άλλα πολλά.", + "SiteSearchSubcategoryHelp3": "Μάθετε περισσότερα στον οδηγό Αναζήτησης Ιστοτόπου.", + "SubmenuPageTitles": "Τίτλοι σελίδων", + "SubmenuPagesEntry": "Σελίδες εισόδου", + "SubmenuPagesExit": "Σελίδες εξόδου", + "SubmenuSitesearch": "Αναζήτηση ιστοσελίδας", + "TimeSpentInReferringAction": "Χρόνος που δαπανήθηκε σε Ενέργεια Αναφορών", + "WidgetEntryPageTitles": "Τίτλοι Σελίδας Εισόδου", + "WidgetExitPageTitles": "Τίτλοι Σελίδας Εξόδου", + "WidgetPageTitles": "Τίτλοι Σελίδων", + "WidgetPageTitlesFollowingSearch": "Τίτλοι Σελίδων που Ακολουθούν μια Αναζήτηση Ιστοσελίδας", + "WidgetPageUrlsFollowingSearch": "Σελίδες που Ακολουθούν μια Αναζήτηση Ιστοσελίδας", + "WidgetPagesEntry": "Σελίδες Εισόδου", + "WidgetPagesExit": "Σελίδες Εξόδου", + "WidgetSearchCategories": "Κατηγορίες Αναζήτησης", + "WidgetSearchKeywords": "Λέξεις-κλειδιά Αναζήτησης Ιστοσελίδας", + "WidgetSearchNoResultKeywords": "Λέξεις-κλειδιά Χωρίς Αποτελέσματα" + } +} diff --git a/plugins/Actions/lang/en.json b/plugins/Actions/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..34ddce7df6915553559aa3a4f639eeb97e58be53 --- /dev/null +++ b/plugins/Actions/lang/en.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "Average based on %1$s hit(s) %2$s between %3$s and %4$s", + "ColumnClickedURL": "Clicked Outlink", + "ColumnClickedURLs": "Clicked Outlinks", + "ColumnActionURL": "Action URL", + "ColumnClicks": "Clicks", + "ColumnClicksDocumentation": "The number of times this link was clicked.", + "ColumnDownloadURL": "Download URL", + "ColumnDownloadURLs": "Download URLs", + "ColumnEntryPageTitle": "Entry Page title", + "ColumnEntryPageURL": "Entry Page URL", + "ColumnEntryPageURLs": "Entry Page URLs", + "ColumnExitPageTitle": "Exit Page Title", + "ColumnExitPageURL": "Exit Page URL", + "ColumnExitPageURLs": "Exit Page URLs", + "ColumnNoResultKeyword": "Keyword with No Search Result", + "ColumnPageName": "Page Name", + "ColumnPagesPerSearch": "Search Results pages", + "ColumnPagesPerSearchDocumentation": "Visitors will search on your website, and sometimes click \"next\" to view more results. This is the average number of search results pages viewed for this keyword.", + "ColumnPageURL": "Page URL", + "ColumnSearchCategory": "Search Category", + "ColumnSearches": "Searches", + "ColumnSearchesDocumentation": "The number of visits that searched for this keyword on your website's search engine.", + "SearchesSegmentHelp": "To select all visits who used internal Site Search, use: %s", + "ColumnSearchExits": "%% Search Exits", + "ColumnSearchExitsDocumentation": "The percentage of visits that left the website after searching for this Keyword on your Site Search engine.", + "ColumnSearchResultsCount": "Search Results Count", + "ColumnSiteSearchKeywords": "Unique Keywords", + "ColumnUniqueClicks": "Unique Clicks", + "ColumnUniqueClicksDocumentation": "The number of visits that involved a click on this link. If a link was clicked multiple times during one visit, it is only counted once.", + "ColumnUniqueDownloads": "Unique Downloads", + "ColumnUniqueOutlinks": "Unique Outlinks", + "ColumnIdPageview": "Pageview ID", + "ColumnInteractionPosition": "Interaction Position", + "ColumnPageViewPosition": "Pageview Position", + "ActionsInVisit": "Actions In Visit", + "DownloadsReportDocumentation": "In this report, you can see which files your visitors have downloaded. %s What Matomo counts as a download is the click on a download link. Whether the download was completed or not isn't known to Matomo.", + "EntryPagesReportDocumentation": "This report contains information about the entry pages that were used during the specified period. An entry page is the first page that a user views during their visit. %s The entry URLs are displayed as a folder structure.", + "EntryPageTitles": "Entry page titles", + "EntryPageTitlesReportDocumentation": "This report contains information about the titles of entry pages that were used during the specified period.", + "ExitPagesReportDocumentation": "This report contains information about the exit pages that occurred during the specified period. An exit page is the last page that a user views during their visit. %s The exit URLs are displayed as a folder structure.", + "ExitPageTitles": "Exit page titles", + "ExitPageTitlesReportDocumentation": "This report contains information about the titles of exit pages that occurred during the specified period.", + "MainMetricsReportDocumentation": "This report provides a very basic overview of what actions your visitors take on your website.", + "OneSearch": "1 search", + "OutlinkDocumentation": "An outlink is a link that leads the visitor away from your website (to another domain).", + "OutlinksReportDocumentation": "This report shows a hierarchical list of outlink URLs that were clicked by your visitors.", + "PagesReportDocumentation": "This report contains information about the page URLs that have been visited. %s The table is organized hierarchically, the URLs are displayed as a folder structure.", + "PageTitlesReportDocumentation": "This report contains information about the titles of the pages that have been visited. %1$s The page title is the HTML %2$s Tag that most browsers show in their window title.", + "PageUrls": "Page URLs", + "PageViewsByVisitor": "Number of times this page was viewed by this visitor across all visits", + "PluginDescription": "Reports about the page views and page titles. Lets you measure your internal website's search engine. Automatically tracks clicks on external links and file downloads.", + "RevisitedPages": "Pages viewed more than once", + "SiteSearchCategories1": "This report lists the Categories that visitors selected when they made a Search on your website.", + "SiteSearchCategories2": "For example, Ecommerce websites typically have a \"Category\" selector so that visitors can restrict their searches to all products in a specific Category.", + "SiteSearchFollowingPagesDoc": "When visitors search on your website, they are looking for a particular page, content, product, or service. This report lists the pages that were clicked the most after an internal search. In other words, the list of pages the most searched for by visitors already on your website.", + "SiteSearchIntro": "Tracking searches that visitors make on your website is a very effective way to learn more about what your audience is looking for, it can help find ideas for new content, new Ecommerce products that potential customers might be searching for, and generally improve the visitors' experience on your website.", + "SiteSearchCategory": "Category (Site Search)", + "SiteSearchCategories": "Categories (Site Search)", + "SiteSearchKeyword": "Keyword (Site Search)", + "SiteSearchKeywords": "Keywords (Site Search)", + "SiteSearchKeywordCount": "Keyword count (Site Search)", + "SiteSearchKeywordCounts": "Keyword counts (Site Search)", + "SiteSearchKeywordsDocumentation": "This report lists the Search Keywords that visitors searched for on your internal Search Engine.", + "SiteSearchKeywordsNoResultDocumentation": "This report lists the Search Keywords that did not return any Search result: maybe the search engine algorithm can be improved, or maybe your visitors are looking for content that is not (yet) on your website?", + "SubmenuPagesEntry": "Entry pages", + "SubmenuPagesExit": "Exit pages", + "SubmenuPageTitles": "Page titles", + "SubmenuSitesearch": "Site Search", + "WidgetEntryPageTitles": "Entry Page Titles", + "WidgetExitPageTitles": "Exit Page Titles", + "WidgetPagesEntry": "Entry Pages", + "WidgetPagesExit": "Exit Pages", + "WidgetPageTitles": "Page Titles", + "WidgetPageTitlesFollowingSearch": "Page Titles Following a Site Search", + "WidgetPageUrlsFollowingSearch": "Pages Following a Site Search", + "WidgetSearchCategories": "Search Categories", + "WidgetSearchKeywords": "Site Search Keywords", + "WidgetSearchNoResultKeywords": "Search Keywords with No Results", + "ActionType": "Action Type", + "ActionTypeSegmentHelp": "A type of action, such as: %s", + "ActionTypes": "Action Types", + "Behaviour": "Behaviour", + "TimeSpentInReferringAction": "Time Spent in Referring Action", + "PagesSubcategoryHelp1": "This report contains information about the page URLs that have been visited.", + "PagesSubcategoryHelp2": "The table is organized hierarchically, the URLs are displayed as a folder structure.", + "PagesSubcategoryHelp3": "Use the plus and minus icons on the left to navigate.", + "EntryPagesSubcategoryHelp1": "This report contains information about the entry pages that were used during the specified period. An entry page is the first page that a user views during their visit.", + "EntryPagesSubcategoryHelp2": "The entry URLs are displayed as a folder structure.", + "ExitPagesSubcategoryHelp1": "This report contains information about the exit pages that occurred during the specified period. An exit page is the last page that a user views during their visit.", + "ExitPagesSubcategoryHelp2": "The exit URLs are displayed as a folder structure.", + "PageTitlesSubcategoryHelp1": "This report contains information about the titles of the pages that have been visited.", + "PageTitlesSubcategoryHelp2": "The page title is the HTML &lt;title&gt; Tag that most browsers show in their window title.", + "SiteSearchSubcategoryHelp1": "The Site Search section shows which keywords visitors use when searching your website. It also displays which pages users view after performing a search and which on-site search keywords return no results at all.", + "SiteSearchSubcategoryHelp2": "These reports can give you ideas about missing content on your site, insight into what your visitors are looking for but can’t find easily, and more.", + "SiteSearchSubcategoryHelp3": "Learn more in the Site Search guide.", + "OutlinksSubcategoryHelp1": "This report shows a hierarchical list of outlink URLs that were clicked by your visitors. An outlink is a link that leads the visitor away from your website (to another domain).", + "DownloadsSubcategoryHelp1": "In this report, you can see which files your visitors have downloaded.", + "DownloadsSubcategoryHelp2": "What Matomo counts as a download is the click on a download link. Whether the download was completed or not isn't known to Matomo." + } +} \ No newline at end of file diff --git a/plugins/Actions/lang/eo.json b/plugins/Actions/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..ec6d972ceab452affbfc7889640b105accbfbf70 --- /dev/null +++ b/plugins/Actions/lang/eo.json @@ -0,0 +1,5 @@ +{ + "Actions": { + "PagesSubcategoryHelp3": "Uzi la plus kaj minus ikonoj maldekstre por foliumi." + } +} diff --git a/plugins/Actions/lang/es-ar.json b/plugins/Actions/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..7b71618a5c09adc181f01bd2c0a34b2e09bf356a --- /dev/null +++ b/plugins/Actions/lang/es-ar.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Tipo de Acción", + "ActionTypes": "Tipos de acción", + "ActionsInVisit": "Acciones en visita", + "AvgGenerationTimeTooltip": "Promedio basado en %1$s acceso(s) %2$s entre %3$s y %4$s", + "Behaviour": "Comportamiento", + "ColumnActionURL": "URL de Acción", + "ColumnClickedURL": "Enlace externo cliqueado", + "ColumnClickedURLs": "Enlaces externos cliqueados", + "ColumnClicks": "Clics", + "ColumnClicksDocumentation": "Número de veces que este enlace ha sido clicado.", + "ColumnDownloadURL": "URL de Descarga", + "ColumnDownloadURLs": "Direcciones web descargadas", + "ColumnEntryPageTitle": "Título de la Página de entrada", + "ColumnEntryPageURL": "URL de la Página de entrada", + "ColumnEntryPageURLs": "Direcciones web de página de entrada", + "ColumnExitPageTitle": "Título de la Página de salida", + "ColumnExitPageURL": "URL de la Página de salida", + "ColumnExitPageURLs": "Direcciones web de página de salida", + "ColumnIdPageview": "ID de paginas vistas", + "ColumnInteractionPosition": "Posición de interacción", + "ColumnNoResultKeyword": "Palabra clave Sin Resultados en la Búsqueda", + "ColumnPageName": "Nombre de Página", + "ColumnPageURL": "URL de Página", + "ColumnPageViewPosition": "Posición de la vista de página", + "ColumnPagesPerSearch": "Páginas de resultados de búsqueda", + "ColumnPagesPerSearchDocumentation": "Los visitantes buscarán en tu sitio web y algunas veces harán click en \"siguiente\" para ver más resultados. Este es el promedio de páginas de resultados de búsqueda, vistos para esta palabra clave.", + "ColumnSearchCategory": "Categoría de búsqueda", + "ColumnSearchExits": "Salidas de búsqueda de %%", + "ColumnSearchExitsDocumentation": "El porcentaje de visitantes que abandonó el sitio después de buscar esta palabra clave en el buscador de tu sitio.", + "ColumnSearchResultsCount": "Cantidad de resultados de la búsqueda", + "ColumnSearches": "Búsquedas", + "ColumnSearchesDocumentation": "La cantidad de visitantes que buscó esta palabra clave en el buscador de tu sitio web.", + "ColumnSiteSearchKeywords": "Palabras clave Únicas", + "ColumnUniqueClicks": "Clics Únicos", + "ColumnUniqueClicksDocumentation": "Número de visitas que hicieron clic en este link. Si hicieron clic a link múltiples veces durante una visita, solo se cuenta como una.", + "ColumnUniqueDownloads": "Descargas Únicas", + "ColumnUniqueOutlinks": "Enlaces de salida Únicos", + "DownloadsReportDocumentation": "En este informe, podés ver qué archivos descargaron tus visitantes. %s Lo que Matomo cuenta como descarga es el clic en un enlace de descarga. Matomo no puede saber si la descarga fue completada.", + "DownloadsSubcategoryHelp1": "En este informe podés ver qué archivos descargaron tus visitantes.", + "DownloadsSubcategoryHelp2": "Lo que Matomo contabiliza como una descarga es el clic en el enlace de descarga. Matomo no sabe si la carga fue completada o no.", + "EntryPageTitles": "Títulos de páginas de entrada", + "EntryPageTitlesReportDocumentation": "Este reporte contiene información sobre los títulos de las páginas de entrada que han sido utilizadas durante el período específico.", + "EntryPagesReportDocumentation": "Este informe contiene información sobre las páginas de entrada que fueron usada durante el período especificado. Una página de entrada es la primera página que un usuario ve durante su visita. %s Las direcciones web de entrada se muestran como una estructura de carpeta.", + "EntryPagesSubcategoryHelp1": "Este informe contiene información sobre las páginas de entradas que fueron usadas durante el período especificado. Una página de entrada es la primer página que un usuario ve durante su visita.", + "EntryPagesSubcategoryHelp2": "Las direcciones web de entrada son mostradas como una estructura de carpeta.", + "ExitPageTitles": "Títulos de páginas de salida", + "ExitPageTitlesReportDocumentation": "Este reporte contiene información sobre los títulos de las páginas de salida que ocurrieron durante el período específico.", + "ExitPagesReportDocumentation": "Este informe contiene información sobre las páginas de salida que ocurrieron durante el período especificado. Una página de salida es la última página que un usuario ve durante su visita. %s Las direcciones web de salida se muestran como una estructura de carpeta.", + "ExitPagesSubcategoryHelp1": "Este informe contiene información sobre las páginas de salida que ocurrieron durante un períoso especificado. Una página de salida es la última página que un usuario ve durante su visita.", + "ExitPagesSubcategoryHelp2": "Las direcciones web de salida son mostradas como una estructura de carpeta.", + "MainMetricsReportDocumentation": "Este informe un pantallazo muy básico de las acciones que tus visitantes hacen en tu sitio web.", + "OneSearch": "1 búsqueda", + "OutlinkDocumentation": "Un enlace externo es un enlace que lleva a su visitante fuera de su página web (a otro dominio).", + "OutlinksReportDocumentation": "Este reporte muestra una lista de los enlaces externos que han sido clicados por sus visitantes, organizada por jerarquía.", + "OutlinksSubcategoryHelp1": "Este informe muestra una lista por jerarquía de enlaces de salida que fueron cliqueadas por tus visitantes. Un enlace de salida es un enlace que lleva al visitante a otro sitio web (a otro dominio).", + "PageTitlesReportDocumentation": "Este reporte contiene información sobre los títulos de las páginas que han sido visitadas. %1$s El título de la página es el Tag %2$s de HTML, que la gran mayoría de los navegadores muestran en el título de su ventana.", + "PageTitlesSubcategoryHelp1": "Este informe contiene información sobre los títulos de las páginas que fueron visitadas.", + "PageTitlesSubcategoryHelp2": "El título de la página es la etiqueta HTML &lt;título&gt; que la mayoría de los navegadores web muestra en el título de la ventana.", + "PageUrls": "URLs de página", + "PageViewsByVisitor": "Número de veces que esta página fue vista por un visitante en todas las visitas", + "PagesReportDocumentation": "Este reporte contiene información sobre las URLs de las páginas que han sido visitadas. %s La tabla está organizada por jerarquía, las URLs han sido mostradas como una estructura de carpetas.", + "PagesSubcategoryHelp1": "Este informe contiene información sobre las direcciones web de la página que fueron visitadas.", + "PagesSubcategoryHelp2": "La tabla es organizada por jerarquía, las direcciones web son mostradas como una estructura de carpeta.", + "PagesSubcategoryHelp3": "Usá los íconos de más y menos en la izquierda para navegar.", + "PluginDescription": "Informes sobre las vistas y títulos de página. Te deja medir tu motor de búsqueda interno del sitio web. Analiza automáticamente los clics en enlaces externos y las descargas de archivos.", + "RevisitedPages": "Páginas vistas más de una vez", + "SiteSearchCategories": "Categorías (búsqueda en el sitio)", + "SiteSearchCategories1": "Este reporte lista las Categorías que los visitantes eligieron cuando realizaron una búsqueda en tu sitio.", + "SiteSearchCategories2": "Por ejemplo: los sitios de E-commerce usualmente tienen un selector de \"Categorías\" para que los visitantes puedan restringir sus búsquedas sólo a los productos de una categoría específica.", + "SiteSearchCategory": "Categoría (búsqueda en el sitio)", + "SiteSearchFollowingPagesDoc": "Cuando los visitantes realizan una búsqueda en tu sitio, están buscando una página, contenido, producto o servicio en particular. Este reporte lista las páginas que más han sido clickeadas luego de una búsqueda interna. En otras palabras, la lista de páginas más buscadas por los visitantes que ya están en de tu sitio.", + "SiteSearchIntro": "El seguimiento de las búsquedas que tus visitantes hacen en tu sitio web es una manera muy efectiva de aprender más acerca de qué busca tu audiencia, puede ayudarte a encontrar ideas para nuevos contenidos o productos que tus potenciales clientes pueden estar buscando y en general, mejorar la experiencia de los visitantes de tu sitio web.", + "SiteSearchKeyword": "Palabra clave (Búsqueda en Sitio)", + "SiteSearchKeywordCount": "Conteo de palabras clave (búsqueda del sitio)", + "SiteSearchKeywordCounts": "Conteos de palabras clave (búsqueda del sitio)", + "SiteSearchKeywords": "Palabras claves (búsqueda en el sitio)", + "SiteSearchKeywordsDocumentation": "Este reporte muestra las palabras clave que tus visitantes buscaron en tu motor de búsqueda interno.", + "SiteSearchKeywordsNoResultDocumentation": "Este reporte lista las palabras clave que no produjeron resultados: ¿se podría mejorar el algoritmo de búsqueda o quizá tus visitantes están buscando algún contenido que no se encuentra (todavía) en tu sitio?", + "SiteSearchSubcategoryHelp1": "La sección de búsqueda de sitio muestra qué palabras claves usaron los visitantes cuando buscaron en tu sitio web. También muestra qué páginas vieron los usuarios después de hacer una búsqueda y qué palabras claves de las búsquedas en el sitio no devolvieron resultados.", + "SiteSearchSubcategoryHelp2": "Estos informes te pueden dar ideas sobre contenido faltante o ausente en tu sitio web, así como ideas sobre lo que tus visitantes estaban buscando pero no pudieron encontrar fácilmente, y mucho más.", + "SiteSearchSubcategoryHelp3": "Aprendé más en la guía de Búsqueda del sitio.", + "SubmenuPageTitles": "Títulos de página", + "SubmenuPagesEntry": "Páginas de entrada", + "SubmenuPagesExit": "Páginas de salida", + "SubmenuSitesearch": "Búsqueda interna", + "TimeSpentInReferringAction": "Tiempo transcurrido en la acción referente", + "WidgetEntryPageTitles": "Títulos de Páginas de Entrada", + "WidgetExitPageTitles": "Títulos de Página de Salida", + "WidgetPageTitles": "Títulos de Páginas", + "WidgetPageTitlesFollowingSearch": "Títulos de las páginas que siguen a una búsqueda interna", + "WidgetPageUrlsFollowingSearch": "Páginas siguientes a una búsqueda interna", + "WidgetPagesEntry": "Páginas de Entrada", + "WidgetPagesExit": "Páginas de Salida", + "WidgetSearchCategories": "Categorías de búsqueda", + "WidgetSearchKeywords": "Palabras clave de las búsquedas internas", + "WidgetSearchNoResultKeywords": "Palabras clave sin resultados" + } +} diff --git a/plugins/Actions/lang/es.json b/plugins/Actions/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..c47dea5aaa2488952dc733755a1feb4a3d9196b0 --- /dev/null +++ b/plugins/Actions/lang/es.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Tipo de acción", + "ActionTypeSegmentHelp": "Un tipo de acción, como: %s", + "ActionTypes": "Tipos de acciones", + "ActionsInVisit": "Acciones en visitas", + "AvgGenerationTimeTooltip": "Promedio basado en %1$s hit(s) %2$s entre %3$s y %4$s", + "Behaviour": "Comportamiento", + "ColumnActionURL": "URL de acción", + "ColumnClickedURL": "Hizo clic en enlace externo", + "ColumnClickedURLs": "Hizo clic en enlaces externos", + "ColumnClicks": "Clics", + "ColumnClicksDocumentation": "El número de veces que se ha hecho clic en este enlace.", + "ColumnDownloadURL": "URL de descarga", + "ColumnDownloadURLs": "URLs de descarga", + "ColumnEntryPageTitle": "Título de la Página de entrada", + "ColumnEntryPageURL": "URL de la página de entrada", + "ColumnEntryPageURLs": "URLs de las páginas de entrada", + "ColumnExitPageTitle": "Título de la Página de salida", + "ColumnExitPageURL": "URL de la página de salida", + "ColumnExitPageURLs": "URLs de las páginas de salida", + "ColumnIdPageview": "ID Páginas vistas", + "ColumnInteractionPosition": "Posición de la interacción", + "ColumnNoResultKeyword": "Palabra clave sin resultados en la búsqueda", + "ColumnPageName": "Nombre de página", + "ColumnPageURL": "URL de la página", + "ColumnPageViewPosition": "Posición de vista de página", + "ColumnPagesPerSearch": "Páginas con resultados de búsqueda", + "ColumnPagesPerSearchDocumentation": "Los visitantes buscarán en su sitio de internet y algunas veces harán clic en \"siguiente\" para ver más resultados. Este es el promedio de páginas de resultados de búsqueda, vistos por esta palabra clave.", + "ColumnSearchCategory": "Categoría de búsqueda", + "ColumnSearchExits": "%% salidas por búsqueda", + "ColumnSearchExitsDocumentation": "El porcentaje de visitantes que dejó el sitio después de buscar esta palabra clave en el buscador interno de su sitio de internet.", + "ColumnSearchResultsCount": "Cantidad de resultados de la búsqueda", + "ColumnSearches": "Búsquedas", + "ColumnSearchesDocumentation": "La cantidad de visitantes que buscaron esta palabra clave en el buscador de su sitio de internet.", + "ColumnSiteSearchKeywords": "Palabras claves únicas", + "ColumnUniqueClicks": "Clics únicos", + "ColumnUniqueClicksDocumentation": "Número de visitas que hicieron clic en este enlace. Si un enlace se le hace clic varias veces durante una sola visita, sólo se cuenta una vez.", + "ColumnUniqueDownloads": "Descargas únicas", + "ColumnUniqueOutlinks": "Enlaces externos únicos", + "DownloadsReportDocumentation": "En este informe, puede ver qué archivos han descargado sus visitantes. %s Lo que Matomo cuenta como descarga es el clic en un enlace de descarga. Si la descarga fue completada o no, Matomo no puede saberlo.", + "DownloadsSubcategoryHelp1": "En este informe puede ver qué ficheros han descargado sus visitantes.", + "DownloadsSubcategoryHelp2": "Lo que Matomo cuenta como una descarga es el clic en un enlace de descarga. Si la descarga se ha completado o no es desconocido por Matomo.", + "EntryPageTitles": "Títulos de páginas de entrada", + "EntryPageTitlesReportDocumentation": "Este informe contiene información sobre los títulos de las páginas de entrada que han sido utilizadas durante el período especificado.", + "EntryPagesReportDocumentation": "Este informe contiene información acerca de las páginas de entrada que fueron usadas durante el periodo de tiempo especificado. Una página de entrada es la primera página que un usuario ve durante su visita. %s Las URL de entrada se muestran como una estructura jerárquica de carpetas.", + "EntryPagesSubcategoryHelp1": "Este informe contiene información respecto a las páginas de entrada utilizadas durante el periodo de tiempo especificado. Una página de entrada es la primera página que un usuario visualiza durante su visita.", + "EntryPagesSubcategoryHelp2": "Las URL-s de entrada se muestran como una estructura de ficheros.", + "ExitPageTitles": "Títulos de páginas de salida", + "ExitPageTitlesReportDocumentation": "Este reporte contiene información sobre los títulos de las páginas de salida que ocurrieron durante el período específicado.", + "ExitPagesReportDocumentation": "Este informe contiene información acerca de las páginas de salida durante el periodo de tiempo especificado. Una página de salida es la última página que un usuario ve durante su visita. %s Las URL de salida se muestran como una estructura jerárquica de carpetas.", + "ExitPagesSubcategoryHelp1": "Este informe contiene información acerca de las páginas de salida durante el periodo de tiempo especificado. Una página de salida es la última página visitada por un usuario durante su visita.", + "ExitPagesSubcategoryHelp2": "Las URL-s de salida se muestran como una estrucura de ficheros.", + "MainMetricsReportDocumentation": "Este informe muestra una vista general básica de las acciones llevadas a cabo por los visitantes en su sitio web.", + "OneSearch": "1 búsqueda", + "OutlinkDocumentation": "Un enlace externo es un enlace que lleva a su visitante fuera de su página de internet (a otro dominio).", + "OutlinksReportDocumentation": "Este informe muestra una lista jerárquica de los enlaces externos en los que sus visitantes han hecho clic.", + "OutlinksSubcategoryHelp1": "Este informe muestra una lista jerárquica de enlaces externos que fueron clicados por sus visitantes. Un enlace externo es un enlace que redirige al visitante fuera de su sitio web (a otro dominio).", + "PageTitlesReportDocumentation": "Este informe contiene información sobre los títulos de las páginas que han sido visitadas. %1$s El título de la página es la etiqueta HTML %2$s, que la gran mayoría de los navegadores muestran en el título de su ventana.", + "PageTitlesSubcategoryHelp1": "Este informe contiene información sobre los títulos de las páginas que han sido visitadas.", + "PageTitlesSubcategoryHelp2": "El título de la página es la etiqueta HTML &lt;title&gt; la cual la mayoría de navegadores muestran en el título de la ventana.", + "PageUrls": "URLs de página", + "PageViewsByVisitor": "Número de veces que esta página fue vista por este visitante durante todas sus visitas", + "PagesReportDocumentation": "Este reporte contiene información sobre las URLs de las páginas que han sido visitadas. %s La tabla está organizada por jerarquía, las URLs han sido mostradas como una estructura de carpetas.", + "PagesSubcategoryHelp1": "Este informe contiene información de las URL de las páginas que han sido visitadas.", + "PagesSubcategoryHelp2": "La tabla está organizada de forma jerárquica, las URL-s se muestran como una estructura de ficheros.", + "PagesSubcategoryHelp3": "Use los iconos de más y menos a la izquierda para navegar.", + "PluginDescription": "Informes sobre las páginas vistas y los títulos de las páginas. Le permite medir el motor de búsqueda de su sitio web interno. Rastrea automáticamente los clics en enlaces externos y descargas de archivos.", + "RevisitedPages": "Páginas vistas más de una vez", + "SearchesSegmentHelp": "Para seleccionar todas las visitas que utilizaron la búsqueda de sitio interna, use: %s", + "SiteSearchCategories": "Categorías (búsqueda en sitio)", + "SiteSearchCategories1": "Este informe lista las Categorías que los visitantes eligieron cuando realizaron una búsqueda en su sitio de internet.", + "SiteSearchCategories2": "Por ejemplo: los sitios de comercio electrónico usualmente tienen un selector de \"Categorías\" para que los visitantes puedan restringir sus búsquedas sólo a los productos de una categoría determinada.", + "SiteSearchCategory": "Categoría (búsqueda en sitio)", + "SiteSearchFollowingPagesDoc": "Cuando los visitantes realizan una búsqueda en su sitio, están buscando una página, contenido, producto o servicio en particular. Este informe enumera las páginas en las que más se han hecho clic luego de una búsqueda interna. En otras palabras, la lista de páginas más buscadas por los visitantes de su sitio.", + "SiteSearchIntro": "El monitoreo de las búsquedas que sus visitantes hacen en su sitio de internet es una manera muy efectiva de aprender más acerca de qué busca su audiencia, puede ayudarle a encontrar ideas para nuevos contenidos o productos que sus potenciales clientes pueden estar buscando y en general, mejorar la experiencia de los visitantes en su sitio de internet.", + "SiteSearchKeyword": "Palabra clave (búsqueda en sitio)", + "SiteSearchKeywordCount": "Contador de palabras clave (búsqueda en sitio)", + "SiteSearchKeywordCounts": "Contadores de palabras clave (búsqueda en sitio)", + "SiteSearchKeywords": "Palabras claves (búsqueda en sitio)", + "SiteSearchKeywordsDocumentation": "Este informe muestra las palabras clave que sus visitantes buscaron en su motor de búsqueda interno.", + "SiteSearchKeywordsNoResultDocumentation": "Este informe enumera las palabras claves de búsquedas que no produjeron resultados: tal vez se pueda mejorar el algoritmo de búsqueda o quizás sus visitantes están buscando algún contenido que no se encuentra (todavía) en tu sitio?", + "SiteSearchSubcategoryHelp1": "La sección Site Search muestra qué palabras claves utilizan tus visitantes al buscar tu sitio web. También muestra que páginas visitan tus usuarios tras realizar una búsqueda y qué palabras clave no devuelven resultados en las busquedas del sitio.", + "SiteSearchSubcategoryHelp2": "Estos informes pueden darle ideas sobre el contenido que falta en su sitio web, una visión sobre lo que sus visitantes están buscando, pero que resulta difícil de encontrar y más.", + "SiteSearchSubcategoryHelp3": "Obtenga más información en la guía de búsqueda en el sitio.", + "SubmenuPageTitles": "Títulos de página", + "SubmenuPagesEntry": "Páginas de entrada", + "SubmenuPagesExit": "Páginas de salida", + "SubmenuSitesearch": "Búsqueda interna", + "TimeSpentInReferringAction": "Tiempo utilizado en la acción referida", + "WidgetEntryPageTitles": "Títulos de páginas de entrada", + "WidgetExitPageTitles": "Títulos de página de egreso", + "WidgetPageTitles": "Títulos de página", + "WidgetPageTitlesFollowingSearch": "Títulos de las páginas que siguen a una búsqueda interna", + "WidgetPageUrlsFollowingSearch": "Páginas siguientes a una búsqueda interna", + "WidgetPagesEntry": "Páginas de entrada", + "WidgetPagesExit": "Páginas de salida", + "WidgetSearchCategories": "Categorías de búsqueda", + "WidgetSearchKeywords": "Palabras claves de las búsquedas internas del sitio", + "WidgetSearchNoResultKeywords": "Búsquedas por palabras clave sin resultados" + } +} diff --git a/plugins/Actions/lang/et.json b/plugins/Actions/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..1e63beb8f89671c6daa91b59419e73373b290a45 --- /dev/null +++ b/plugins/Actions/lang/et.json @@ -0,0 +1,44 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "Keskmine, mis baseerub %1$s laadimisel %2$s, %3$s ja %4$s vahel.", + "ColumnClicks": "Hiireklikke", + "ColumnClicksDocumentation": "Arv, mitu korda antud linki on klikitud.", + "ColumnDownloadURL": "Allalaadimise URL", + "ColumnEntryPageTitle": "Sisenetud lehe pealkiri", + "ColumnEntryPageURL": "Sisenetud lehe URL", + "ColumnExitPageTitle": "Väljutud lehe pealkiri", + "ColumnExitPageURL": "Väljutud lehe URL", + "ColumnNoResultKeyword": "Ilma tulemusteta võtmesõna", + "ColumnPageName": "Lehe nimi", + "ColumnPageURL": "Lehe URL", + "ColumnPagesPerSearch": "Otsingu tulemuste lehed", + "ColumnSearchCategory": "Otsingu Kategooria", + "ColumnSearchResultsCount": "Otisngutulemuste arv", + "ColumnSearches": "Otsinguid", + "ColumnSearchesDocumentation": "Külastuste arv, mille jooksul otsiti antud märksõna veebilehe enda otsingumootoriga.", + "ColumnSiteSearchKeywords": "Unikaalseid Võtmesõnu", + "ColumnUniqueClicks": "Unikaalseid Klikke", + "ColumnUniqueDownloads": "Unikaalseid Allalaadimisi", + "ColumnUniqueOutlinks": "Unikaalsed väljuvad lingid", + "EntryPageTitles": "Sisenetud lehtede pealkirjad", + "ExitPageTitles": "Väljutud lehtede pealkirjad", + "OneSearch": "1 otsing", + "PageUrls": "Lehe URL'id", + "PagesSubcategoryHelp3": "Kasuta pluss ja miinus ikoone vasakul, et navigeerida", + "SiteSearchKeyword": "Võtmesõna (lehe otsing)", + "SubmenuPageTitles": "Lehe pealkirjad", + "SubmenuPagesEntry": "Sisenetud lehed", + "SubmenuPagesExit": "Väljutud lehed", + "SubmenuSitesearch": "Lehe otsing", + "WidgetEntryPageTitles": "Sisenetud lehtede pealkirjad", + "WidgetExitPageTitles": "Väljutud lehtede pealkirjad", + "WidgetPageTitles": "Lehe pealkirjad", + "WidgetPageTitlesFollowingSearch": "Peale otsingut külastatud lehtede pealkirjad", + "WidgetPageUrlsFollowingSearch": "Peale otsingut külastatud lehed", + "WidgetPagesEntry": "Sisenetud lehed", + "WidgetPagesExit": "Lehed kust väljutakse", + "WidgetSearchCategories": "Otsingu Kategooriad", + "WidgetSearchKeywords": "Lehe otsingu võtmesõnad", + "WidgetSearchNoResultKeywords": "Ilma tulemusteta otsingu võtmesõnad" + } +} diff --git a/plugins/Actions/lang/eu.json b/plugins/Actions/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..3e36389dfe0d1d5def718ddfde514ecda42a1098 --- /dev/null +++ b/plugins/Actions/lang/eu.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Ekintza-mota", + "ActionTypes": "Ekintza-motak", + "ActionsInVisit": "Ekintzak bisitan", + "AvgGenerationTimeTooltip": "%1$s bisitako ekintzetan oinarritutako batezbestekoa %2$s %3$s eta %4$s artean", + "Behaviour": "Portaera", + "ColumnActionURL": "Ekintzaren URLa", + "ColumnClickedURL": "Kanpo estekan klik eginda", + "ColumnClickedURLs": "Kanpo esteketan klik eginda", + "ColumnClicks": "Klik kopurua", + "ColumnClicksDocumentation": "Zenbat aldiz egin den klik esteka honetan.", + "ColumnDownloadURL": "Deskargaren URLa", + "ColumnDownloadURLs": "Deskargaren URLak", + "ColumnEntryPageTitle": "Sarrera-orriaren izenburua", + "ColumnEntryPageURL": "Sarrera-orriaren URLa", + "ColumnEntryPageURLs": "Sarrera-orriaren URLak", + "ColumnExitPageTitle": "Irteera-orriaren izenburua", + "ColumnExitPageURL": "Irteera-orriaren URLa", + "ColumnExitPageURLs": "Irteera-orriaren URLak", + "ColumnIdPageview": "Orri-ikustaldi ID", + "ColumnInteractionPosition": "Interakzioaren kokapena", + "ColumnNoResultKeyword": "Bilaketa emaitzarik gabeko gako-hitza", + "ColumnPageName": "Orriaren izena", + "ColumnPageURL": "Orriaren URLa", + "ColumnPageViewPosition": "Orri-ikustaldiaren posizioa", + "ColumnPagesPerSearch": "Bilaketaren emaitzen orriak", + "ColumnPagesPerSearchDocumentation": "Bisitariek bilaketak egingo dituzte zure webgunean, eta batzuetan \"hurrengoa\" estekan klik egingo dute emaitza gehiago ikusteko. Hau da gako-hitz honetarako bilaketa-emaitzetan ikusitako orrien batez besteko kopurua.", + "ColumnSearchCategory": "Bilaketa kategoria", + "ColumnSearchExits": "%% Bilaketa irteerak", + "ColumnSearchExitsDocumentation": "Gako-hitz hau zure gunearen bilaketa-motorrean bilatu ondoren webgunea utzi duten bisitarien ehunekoa.", + "ColumnSearchResultsCount": "Bilaketaren emaitzen kopurua", + "ColumnSearches": "Bilaketak", + "ColumnSearchesDocumentation": "Zenbat bisitetan bilatu zen gako-hitz hau zure webguneko bilatzailean.", + "ColumnSiteSearchKeywords": "Gako-hitz desberdinak", + "ColumnUniqueClicks": "Klik desberdinak", + "ColumnUniqueClicksDocumentation": "Zenbat bisitak egin duten klik esteka honetan. Esteka batean behin baino gehiagotan egin bazen klik bisita batean, soilik behin kontatzen da.", + "ColumnUniqueDownloads": "Deskarga desberdinak", + "ColumnUniqueOutlinks": "Kanpo esteka desberdinak", + "DownloadsReportDocumentation": "Txosten honetan ikus dezakezu zein fitxategi deskargatu dituzten bisitariek. %s Matomok deskarga bezala kontatzen du deskargarako estekan klik egitea. Matomok ez daki deskarga osatu zen ala ez.", + "DownloadsSubcategoryHelp1": "Txosten honetan bisitariek deskargatutako fitxategiak ikus ditzakezu.", + "DownloadsSubcategoryHelp2": "Matomok deskarga bezala kontatzen ditu deskarga-esteketan egindako klikak. Matomok ez daki deskarga osatu den ala ez.", + "EntryPageTitles": "Sarrera-orrien izenburuak", + "EntryPageTitlesReportDocumentation": "Txosten honek zehaztutako epean erabilitako sarrera-orrien izenburuei buruzko informazioa dauka.", + "EntryPagesReportDocumentation": "Txosten honek zehaztutako epean erabilitako sarrera-orriei buruzko informazioa dauka. Sarrera-orri bat erabiltzaileak bisita batean ikusten duen lehen orria da. %s Sarrerako URLak karpeta egiturarekin bistaratzen dira.", + "EntryPagesSubcategoryHelp1": "Txosten honek zehaztutako epean erabilitako sarrera-orriei buruzko informazioa dauka. Sarrera-orri bat erabiltzaileak bisita batean ikusten duen lehen orria da.", + "EntryPagesSubcategoryHelp2": "Sarrerako URLak karpeta egituran bistaratzen dira.", + "ExitPageTitles": "Irteera-orrien izenburuak", + "ExitPageTitlesReportDocumentation": "Txosten honek zehaztutako epean erabilitako irteera-orrien izenburuei buruzko informazioa dauka.", + "ExitPagesReportDocumentation": "Txosten honek zehaztutako epean erabilitako irteera-orriei buruzko informazioa dauka. Irteera-orri bat erabiltzaile batek bisita batean ikusten duen azken orria da. %s Irteerako URLak karpeta egiturarekin bistaratzen dira.", + "ExitPagesSubcategoryHelp1": "Txosten honek zehaztutako epean erabilitako irteera-orriei buruzko informazioa dauka. Irteera-orri bat erabiltzaileak bisita batean ikusten duen azken orria da.", + "ExitPagesSubcategoryHelp2": "Irteerako URLak karpeta egituran bistaratzen dira.", + "MainMetricsReportDocumentation": "Txosten honek bisitariek zure webgunean gauzatzen dituzten ekintzen oinarrizko ikuspegi orokorra eskaintzen du.", + "OneSearch": "Bilaketa 1", + "OutlinkDocumentation": "Kanpo esteka bat bisitaria zure webgunetik kanpora eramaten duen esteka bat da (beste domeinu batera).", + "OutlinksReportDocumentation": "Bisitariek klik egindako kanpo esteken URLen zerrenda hierarkikoa erakusten du txosten honek.", + "OutlinksSubcategoryHelp1": "Bisitariek klik egindako kanpo esteken URLen zerrenda hierarkikoa erakusten du txosten honek. Bisitaria zure webgunetik kanpora (beste domeinu batera) eramaten duen esteka da kanpo esteka bat.", + "PageTitlesReportDocumentation": "Txosten honek bisitatutako orrien izenburuei buruzko informazioa dauka. %1$s Orriaren izenburua HTML %2$s etiketa da, nabigatzaile gehienek leihoaren izenburuan bistaratzen dutena.", + "PageTitlesSubcategoryHelp1": "Txosten honek informazioa dauka bisitatutako orrien izenburuei buruz.", + "PageTitlesSubcategoryHelp2": "Orriaren izenburua HTML &lt;title&gt; etiketa da, nabigatzaile gehienek leihoaren izenburuan bistaratzen dute.", + "PageUrls": "Orrien URLak", + "PageViewsByVisitor": "Orri hau zenbat aldiz ikusi duen bisitari honek, bere bisita guztiak kontuan hartuta", + "PagesReportDocumentation": "Txosten honek bisitatutako orrien URLei buruzko informazioa dauka. %s Taula hierarkikoki antolatuta dago, URLak karpeta egituran bistaratzen dira.", + "PagesSubcategoryHelp1": "Txosten honek informazioa ematen du bisitatutako orrien URLei buruz.", + "PagesSubcategoryHelp2": "Taula hau hierarkikoki antolatuta dago, URLak karpeta egitura moduan bistaratzen dira.", + "PagesSubcategoryHelp3": "Erabili ezkerreko plus eta minus ikonoak nabigatzeko.", + "PluginDescription": "Orri-ikustaldien eta orrien izenburuen berri ematen du. Zure webguneko barne-bilatzailearen erabilera neurtzeko aukera ematen dizu. Automatikoki erregistratzen ditu kanpo estekak eta fitxategien deskargak.", + "RevisitedPages": "Behin baino gehiagotan ikusitako orriak", + "SiteSearchCategories": "Kategoriak (gunearen bilaketa)", + "SiteSearchCategories1": "Bisitariek zure webgunean bilaketak bat egin dituztenean hautatutako kategorien zerrenda dago txosten honetan.", + "SiteSearchCategories2": "Adibidez, merkataritza elektronikoko webguneek \"Kategoria\" hautatzaile bat izan ohi dute, bisitariek bilaketak kategoria bateko produktuetara mugatzeko aukera izan dezaten.", + "SiteSearchCategory": "Kategoria (gunearen bilaketa)", + "SiteSearchFollowingPagesDoc": "Bisitariek zure webgunean bilaketak egiten dituztenean orri, eduki, produktu edo zerbitzu zehatz baten bila ari dira. Txosten honetan ikus ditzakezu barne-bilaketa baten ondoren gehienetan klik egindako orriak. Beste modu batera esanda, dagoeneko zure webgunean dauden bisitariek gehien bilatutako orrien zerrenda.", + "SiteSearchIntro": "Bisitariek zure webgunean egiten dituzten bilaketen jarraipena egitea oso modu eraginkorra da zure audientzia zeren bila dabilen ikasteko, eduki berria sortzeko ideiak eman ditzake, baita bezero potentzialek bilatzen dituzten merkataritza elektronikoko produktu berriak ezagutzeko aukera ere. Orokorrean, bisitariek zure webgunean duten esperientzia hobetzen lagundu dezake.", + "SiteSearchKeyword": "Gako-hitza (gunearen bilaketa)", + "SiteSearchKeywordCount": "Gako-hitz kopurua (gunearen bilaketa)", + "SiteSearchKeywordCounts": "Gako-hitz kopuruak (gunearen bilaketa)", + "SiteSearchKeywords": "Gako-hitzak (gunearen bilaketa)", + "SiteSearchKeywordsDocumentation": "Txosten honetan zerrendatzen dira bisitariek zure barne-bilatzailean bilatutako gako-hitzak.", + "SiteSearchKeywordsNoResultDocumentation": "Txosten honetan zerrendatzen dira emaitzarik itzuli ez duten bilaketako gako-hitzak: behar bada bilatzailearen algoritmoa hobetu daiteke, edo baliteke bisitariek zure webgunean (oraindik) ez dagoen edukia bilatu izana?", + "SiteSearchSubcategoryHelp1": "Guneko bilaketa atalak erakusten ditu bisitariek zure webgunean bilatu dituzten gako-hitzak. Bisitariek bilaketa egin ondoren ikusi dituzten orriak ere bistaratzen ditu, baita emaitzarik itzuli ez duten gako-hitzak ere.", + "SiteSearchSubcategoryHelp2": "Txosten hauetatik zure webgunean falta diren edukiei buruzko ideiak jaso ditzakezu, bisitariek bilatzen duten baina aurkitu ezin dutena ezagutu, eta gehiago.", + "SiteSearchSubcategoryHelp3": "Ikasi gehiago Guneko bilaketa gidan.", + "SubmenuPageTitles": "Orrien izenburuak", + "SubmenuPagesEntry": "Sarrera-orriak", + "SubmenuPagesExit": "Irteera-orriak", + "SubmenuSitesearch": "Gunearen bilaketa", + "TimeSpentInReferringAction": "Erreferentzia ekintzan igarotako denbora", + "WidgetEntryPageTitles": "Sarrera-orrien izenburuak", + "WidgetExitPageTitles": "Irteera-orrien izenburuak", + "WidgetPageTitles": "Orri-izenburuak", + "WidgetPageTitlesFollowingSearch": "Orri-izenburuak gunearen bilaketa baten ondoren", + "WidgetPageUrlsFollowingSearch": "Orriak gunearen bilaketa baten ondoren", + "WidgetPagesEntry": "Sarrera-orriak", + "WidgetPagesExit": "Irteera-orriak", + "WidgetSearchCategories": "Bilaketa kategoriak", + "WidgetSearchKeywords": "Gunearen bilaketaren gako-hitzak", + "WidgetSearchNoResultKeywords": "Emaitzarik gabeko bilaketen gako-hitzak" + } +} diff --git a/plugins/Actions/lang/fa.json b/plugins/Actions/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..3414d6c5fcf32af7456cfa1277f6de5e60a9aeb9 --- /dev/null +++ b/plugins/Actions/lang/fa.json @@ -0,0 +1,79 @@ +{ + "Actions": { + "ActionType": "نوع فعالیت", + "ActionTypes": "نوع فعالیت", + "ActionsInVisit": "فعالیت‌های زمان بازدید", + "AvgGenerationTimeTooltip": "میانگین بر اساس %1$s کلیک %2$s بین %3$s و %4$s", + "Behaviour": "رفتار", + "ColumnActionURL": "آدرس فعالیت", + "ColumnClicks": "کلیک ها", + "ColumnClicksDocumentation": "تعداد دفعاتی که بر روی این لینک کلیک شده است.", + "ColumnDownloadURL": "آدرس دانلود شده", + "ColumnDownloadURLs": "آدرس‌های دانلود", + "ColumnEntryPageTitle": "عنوان صفحات ورودی", + "ColumnEntryPageURL": "آدرس صفحات ورودی", + "ColumnEntryPageURLs": "آدرس‌های صفحه ورودی", + "ColumnExitPageTitle": "عنوان صفحات خروجی", + "ColumnExitPageURL": "آدرس صفحات خروجی", + "ColumnExitPageURLs": "آدرس‌های صفحه خروجی", + "ColumnIdPageview": "آی‌دی نمایش صفحه", + "ColumnInteractionPosition": "موقعیت تعامل", + "ColumnNoResultKeyword": "کلیدواژه بدون نتیجه جستحو", + "ColumnPageName": "نام صفحات", + "ColumnPageURL": "آدرس صفحه", + "ColumnPagesPerSearch": "صفحات نتایج جستجو", + "ColumnPagesPerSearchDocumentation": "جستجوی نوشتارها", + "ColumnSearchCategory": "دسته بندی جستجو", + "ColumnSearchExits": "%% خروج‌ها جستجو", + "ColumnSearchExitsDocumentation": "نوشتار خروجی", + "ColumnSearchResultsCount": "تعداد نتایج جستجو", + "ColumnSearches": "جستجوها", + "ColumnSearchesDocumentation": "جستجوی نوشتار ها", + "ColumnSiteSearchKeywords": "کلیدواژه های یکتا", + "ColumnUniqueClicks": "کلیک های منحصر به فرد", + "ColumnUniqueClicksDocumentation": "تعداد بار مشاهده شده است که شامل یک کلیک بر روی این لینک. اگر یک لینک چند بار در طول یک دیدار کلیک شد، آن است که تنها یک بار شمارش است.", + "ColumnUniqueDownloads": "دانلودهای منحصربفرد", + "ColumnUniqueOutlinks": "لینک های خروجی منحصر به فرد", + "DownloadsReportDocumentation": "در این جا شما میتوانید گزارش فایل هایی را که بازدید کنندگان شما دانلود کرده اند را ببینید . %s به چه تعداد روی لینک دانلود کلیک شده است. اینکه ایا دانلود کامل انجام شده یا نصفه به پایان رسیده.", + "EntryPageTitles": "ورود به عنوان های صفحه", + "EntryPageTitlesReportDocumentation": "این گزارش شامل اطلاعاتی درباره عنوان صفحه های ورودی می باشد که در مدت مشخصی استفاده شده اند.", + "EntryPagesReportDocumentation": "این گزارش شامل اطلاعاتی درباره‌ی صفحه ورودی که در بازه معین استفاده شده‌ است، می‌باشد. صفحه‌ ورودی اولین صفحه‌ای است که کاربر در هنگام بازدید مشاهده می‌کند. %s آدرس‌های ورودی به عنوان یک ساختار پوشه‌بندی نمایش داده می‌شوند.", + "ExitPageTitles": "خروج از عنوان های صفحه", + "ExitPageTitlesReportDocumentation": "این گزارش شامل اطلاعاتی درباره عنوان های صفحه های خروجی می باشد که در مدت مشخصی اتفاق افتاده است.", + "ExitPagesReportDocumentation": "این گزارش شامل اطلاعاتی درباره‌ی صفحات خروج که در بازه معین استفاده شده‌ است، می‌باشد. صفحه خروجی آخرین صفحه‌ای است که کاربر در هنگام بازدید مشاهده می‌کند. %s آدرس‌های خروج به عنوان یک ساختار پوشه‌بندی نمایش داده می‌شوند.", + "OneSearch": "1 جستجو", + "OutlinkDocumentation": "لینک یک لینک که منجر بازدید کننده از وب سایت خود را به یکی دیگر از دامنه است.", + "OutlinksReportDocumentation": "این گزارش نشان می دهد یک لیست سلسله مراتبی آدرس از لینک که کلیک بازدید کنندگان شما شد.", + "PageTitlesReportDocumentation": "این گزارش شامل اطلاعاتی درباره عنوان صفحه هایی است که بازدید شده اند. %1$s عنوان صفحه تگ %2$s HTML است که اغلب مرورگرها در عنوان پنجره هایشان نمایش می دهند.", + "PageUrls": "آدرس صفحات", + "PageViewsByVisitor": "تعدادد دفعاتی که این صفحه توسط این بازدیدکننده در طول تمام بازدیدهایش مشاهده شده.", + "PagesReportDocumentation": "این گزارش شامل اطلاعاتی درباره URLهای صفحه هایی است که بازدید شده اند. %s جدول به صورت سلسله مراتبی سازماندهی شده است و URLها به عنوان پوشه نمایش داده شده اند.", + "PagesSubcategoryHelp3": "ازآیکون مثبت و منفی بر روی نوار سمت چپ برای حرکت استفاده کنید.", + "PluginDescription": "گزارش‌ها درباره بازدید صفحات و عنوان صفحات. شما می‌توانید با استفاده از این بخش موتور جستجوی داخلی وبسایت را ارزیابی کنید. به صورت خودکار کلیک‌های لینک‌های خارجی و دانلود فایل را رهگیری می‌کند.", + "RevisitedPages": "صفحات بیش از یک بار مشاهده شده‌اند", + "SiteSearchCategories": "دسته‌بندی‌ها (جستجوی سایت)", + "SiteSearchCategories1": "این گزارش مجموعه هایی را فهرست می کند که بازدیدکنندگان هنگام جستجو در سایت شما , آن را انتخاب نموده اند.", + "SiteSearchCategories2": "برای نمونه , وبسایت های تجاری معمولا \"مجموعه\"هایی برای انتخاب دارند که به بازدیدکنندگان اجازه می دهد جستجوهایشان را از تمام محصولات به مجموعه ای از محصولات محدود کنند.", + "SiteSearchCategory": "دسته‌بندی (جستجوی سایت)", + "SiteSearchFollowingPagesDoc": "هنگامی که بازدیدکنندگان در سایت شما جستجو می کنند , آنها به دنبال صفحه ,محتوی ,محصول و یا سرویس خاصی هستند. این گزارش صفحاتی را فهرست می کند که بیشترین کلیک را بعد از جستجوی داخلی داشته اند. به عبارت دیگر ,فهرست صفحه هایی که بیشترین جستجو را توسط بازدیدکنندگان سایت شما داشته اند.", + "SiteSearchIntro": "ردیابی جستجوهایی که بازدیدکنندگان روی سایت شما انجام داده اند , یک راه بسیار موثر برای آگاهی از چیزهایی است که مخاطبان شما به دنبال آن می گردند. این می تواند به شما کمک کند که ایده هایی برای محتوای جدید, محصولات تجاری تازه ای پیدا کنید که مشتریان ممکن است به دنبال آنها بوده اند و به طو کلی حضور بازدیدکنندگان در سایت شما را ارتقا می دهد.", + "SiteSearchKeyword": "(کلمه (جستجوی سایت", + "SiteSearchKeywords": "کلیدواژه‌ها (جستجوی سایت)", + "SiteSearchKeywordsDocumentation": "این گزارش واژه های کلیدی را فهرست می کند که بازدیدکنندگان در جستجوی داخلی سایت شما استفاده کرده اند .", + "SiteSearchKeywordsNoResultDocumentation": "این گزارش واژه های کلیدی را فهرست می کند که هیچ پاسخی برای جستجو نداشته اند : ممکن است الگوریتم جستجوی سایت شما باید ارتقا پیدا کند و یا شاید بازدیدکنندگان به دنبال مطلبی بوده اند که در سایت شما (هنوز) وجود ندارد?", + "SubmenuPageTitles": "عناوین صفحه ها", + "SubmenuPagesEntry": "صفحه های ورودی", + "SubmenuPagesExit": "صفحه های خروجی", + "SubmenuSitesearch": "جستجوی سایت", + "WidgetEntryPageTitles": "عنوان صفحه ورودی", + "WidgetExitPageTitles": "عنوان صفحه خروجی", + "WidgetPageTitles": "عنوان های صفحه", + "WidgetPageTitlesFollowingSearch": "عنوان های صفحه پیرو جستجوی سایت", + "WidgetPageUrlsFollowingSearch": "عنوان های صفحه پیرو جستجوی سایت", + "WidgetPagesEntry": "صفحات ورودی", + "WidgetPagesExit": "صفحه های خروجی", + "WidgetSearchCategories": "جستجوی بخش ها", + "WidgetSearchKeywords": "کلیدواژه های جستجوی سایت", + "WidgetSearchNoResultKeywords": "جستجوی کلیدواژه ها بدون نتیجه بود" + } +} diff --git a/plugins/Actions/lang/fi.json b/plugins/Actions/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..337bc803198f4f388ef2b897469e2e44f73e2722 --- /dev/null +++ b/plugins/Actions/lang/fi.json @@ -0,0 +1,79 @@ +{ + "Actions": { + "ActionType": "Toiminnon tyyppi", + "ActionTypes": "Toimintotyypit", + "ActionsInVisit": "Toimintoja käynnin aikana", + "AvgGenerationTimeTooltip": "Keskiarvo %1$s osumasta %2$s %3$s - %4$s", + "Behaviour": "Käyttäytyminen", + "ColumnActionURL": "Toiminnon osoite", + "ColumnClicks": "Klikkaukset", + "ColumnClicksDocumentation": "Tämän linkin klikkausten määrä.", + "ColumnDownloadURL": "Ladattu osoite", + "ColumnDownloadURLs": "Latausosoitteet", + "ColumnEntryPageTitle": "Saapumissivun otsikko", + "ColumnEntryPageURL": "Saapumissivun osoite", + "ColumnEntryPageURLs": "Saapumissivun osoitteet", + "ColumnExitPageTitle": "Poistumissivun otsikko", + "ColumnExitPageURL": "Poistumissivun osoite", + "ColumnExitPageURLs": "Poistumissivun osoitteet", + "ColumnIdPageview": "Sivukatselun ID", + "ColumnInteractionPosition": "Vuorovaikutuksen paikka", + "ColumnNoResultKeyword": "Ei hakutuloksia", + "ColumnPageName": "Sivun nimi", + "ColumnPageURL": "Sivun osoite", + "ColumnPagesPerSearch": "Haun tulossivut", + "ColumnPagesPerSearchDocumentation": "Kävijät tekevät sivustolla hakuja ja klikkaavat toisinaan linkkejä tuloksien seuraaville sivuille. Tämä on keskiarvo katsotuista hakutulossivuista.", + "ColumnSearchCategory": "Haun kategoria", + "ColumnSearchExits": "%% poistumista hausta", + "ColumnSearchExitsDocumentation": "Osuus käynneistä jotka päättyivät tämän hakusanan hakemiseen sivun hakukoneella.", + "ColumnSearchResultsCount": "Hakutulosten määrä", + "ColumnSearches": "Haut", + "ColumnSearchesDocumentation": "Käynnit, joilla haettiin tällä hakusanalla.", + "ColumnSiteSearchKeywords": "Uniikkeja hakusanoja", + "ColumnUniqueClicks": "Uniikit klikkaukset", + "ColumnUniqueClicksDocumentation": "Käynnit, jotka sisälsivät tämän linkin klikkaamisen. Vaikka sama kävijä seuraisi linkkiä useita kertoja, se lasketaan vain kerran.", + "ColumnUniqueDownloads": "Uniikit lataukset", + "ColumnUniqueOutlinks": "Yksilölliset lähtevät linkit", + "DownloadsReportDocumentation": "Tässä raportissa näet, mitä tiedostoja kävijät ovat ladanneet. %s Matomo laskee lataukseksi latauslinkin klikkaamisen. Matomo ei tiedä, valmistuiko lataus.", + "EntryPageTitles": "Saapumissivujen otsikot", + "EntryPageTitlesReportDocumentation": "Tämä raportti sisältää tietoa saapumissivujen otsikoista annetulla aikavälillä.", + "EntryPagesReportDocumentation": "Tämä raportti sisältää tietoa saapumissivuista, joita on käytetty annetulla aikavälillä. Saapumissivu on ensimmäinen sivu, jota käyttäjä on katsonut käyntinsä aikana. %s Saapumis-URL:t näytetään kansiorakenteena.", + "ExitPageTitles": "Poistumissivujen otsikot", + "ExitPageTitlesReportDocumentation": "Tämä raportti sisältää tietoa poistumissivujen otsikoista annetulla aikavälillä.", + "ExitPagesReportDocumentation": "Tämä raportti sisältää tietoa poistumissivuista annetulla aikavälillä. Poistumissivu on viimeinen sivu, jota käyttäjä on katsonut käyntinsä aikana. %s Poistumis-URL:t näytetään kansiorakenteena.", + "OneSearch": "1 haku", + "OutlinkDocumentation": "Lähtevä linkki on linkki, joka vie kävijän pois sinun sivultasi (eri verkkotunnukseen).", + "OutlinksReportDocumentation": "Tämä raportti näyttää hierarkisen listan lähtevistä osoitteista, joita kävijät ovat seuranneet.", + "PageTitlesReportDocumentation": "Tämä raportti sisältää tietoa käytyjen sivujen otsikoista. %1$s Sivun otsikko on HTML:n %2$s-tagi, jonka useimmat selaimet näyttävät ikkunan otsikossa.", + "PageUrls": "Sivujen osoitteet", + "PageViewsByVisitor": "Kuinka monta kertaa tämä kävijä on katsonut sivua kaikkien käyntiensä aikana", + "PagesReportDocumentation": "Tämä raportti sisältää tietoa sivuista, joilla on käyty. %s Taulukko on organisoitu hierarkisesti, URL:t näytetään puurakenteena.", + "PagesSubcategoryHelp3": "Käytä plus- ja miinus-merkkejä vasemmalla.", + "PluginDescription": "Raportoi sivukatselut ja sivujen otsikot. Mahdollistaa sivun sisäisen hakukoneen mittaamisen. Seuraa automaattisesti ulospäin suuntautuvia linkkejä ja latauksia.", + "RevisitedPages": "Sivut, joita on katsottu useita kertoja", + "SiteSearchCategories": "Kategoriat (sivuhaku)", + "SiteSearchCategories1": "Tämä raportti listaa kategoriat jotka kävijät valitsivat.", + "SiteSearchCategories2": "Esimerkiksi e-kauppojen sivuilla on tyypillisesti \"Kategoria\"-valitsin, jolla käyttäjät voivat rajoittaa hakutuloksia.", + "SiteSearchCategory": "Kategoria (sivuhaku)", + "SiteSearchFollowingPagesDoc": "Kun kävijät hakevat sivuiltasi, he etsivät tiettyä sivua, sisältöä, tuotetta tai palvelua. Tämä raportti listaa sivut joita klikattiin useiten haun jälkeen.", + "SiteSearchIntro": "Kävijöiden hakujen seuraaminen on tehokas tapa kerätä tietoa siitä, mitä kävijäsi haluavat. Tämä voi auttaa uuden sisällön tai uusien tuotteiden ideoinnissa, joita potentiaaliset asiakkaat voivat etsiä. Voit myös yleisesti parantaa kävijöiden kokemusta verkkosivullasi.", + "SiteSearchKeyword": "Avainsana (sivuhaku)", + "SiteSearchKeywords": "Avainsanat (sivuhaku)", + "SiteSearchKeywordsDocumentation": "Tämä raportti sisältää hakusanat, joita kävijäsi käyttivät sisäisessä hakukoneessasi.", + "SiteSearchKeywordsNoResultDocumentation": "Tämä raportti sisältää hakusanat, jotka eivät antaneet mitään tulosta. Ehkä hakukonetta voidaan kehittää tai kenties kävijäsi etsivät sisältöä, jota ei (vielä) löydy verkkosivultasi?", + "SubmenuPageTitles": "Sivujen otsikot", + "SubmenuPagesEntry": "Saapumissivut", + "SubmenuPagesExit": "Poistumissivut", + "SubmenuSitesearch": "Sivun haku", + "WidgetEntryPageTitles": "Saapumissivujen otsikot", + "WidgetExitPageTitles": "Poistumissivujen otsikot", + "WidgetPageTitles": "Sivujen otsikot", + "WidgetPageTitlesFollowingSearch": "Verkkosivuhaun tuloksena löytyneet sivujen otsikot", + "WidgetPageUrlsFollowingSearch": "Verkkosivuhaun tuloksena löytyneet sivut", + "WidgetPagesEntry": "Saapumissivut", + "WidgetPagesExit": "Poistumissivut", + "WidgetSearchCategories": "Haun kategoriat", + "WidgetSearchKeywords": "Sivuhaun hakusanat", + "WidgetSearchNoResultKeywords": "Hakusanat ilman tuloksia" + } +} diff --git a/plugins/Actions/lang/fr.json b/plugins/Actions/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..beea9ddf085af98481fb641ebe03ef703e839d24 --- /dev/null +++ b/plugins/Actions/lang/fr.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Type d'action", + "ActionTypeSegmentHelp": "Un type d'action tel que : %s", + "ActionTypes": "Types d'action", + "ActionsInVisit": "Nombre d'actions de la visite", + "AvgGenerationTimeTooltip": "Moyenne basée sur %1$s entrée(s) %2$s entre %3$s et %4$s", + "Behaviour": "Comportement", + "ColumnActionURL": "URL d'action", + "ColumnClickedURL": "Lien sortant cliqué", + "ColumnClickedURLs": "Liens sortants cliqués", + "ColumnClicks": "Clics", + "ColumnClicksDocumentation": "Nombre de clics sur ce lien.", + "ColumnDownloadURL": "URL de téléchargement", + "ColumnDownloadURLs": "URL's téléchargées", + "ColumnEntryPageTitle": "Titre de la page d'entrée", + "ColumnEntryPageURL": "URL de la page d'entrée", + "ColumnEntryPageURLs": "URL's de page d'entrée", + "ColumnExitPageTitle": "Titre de la page de sortie", + "ColumnExitPageURL": "URL de la page de sortie", + "ColumnExitPageURLs": "URL's de page de sortie", + "ColumnIdPageview": "ID de vue de page", + "ColumnInteractionPosition": "Position de l'action", + "ColumnNoResultKeyword": "Mots-clés sans résultats de recherche", + "ColumnPageName": "Titre de la page", + "ColumnPageURL": "URL de la page", + "ColumnPageViewPosition": "Position de la page vue", + "ColumnPagesPerSearch": "Pages des résultats de la recherche", + "ColumnPagesPerSearchDocumentation": "Les visiteurs vont effectuer des recherches sur votre site web et parfois cliquer sur \"suivant\" pour afficher plus de résultats. Ceci est le nombre moyen de pages de résultats vues pour ce mot-clé.", + "ColumnSearchCategory": "Catégorie de recherche", + "ColumnSearchExits": "%% sorties de recherche", + "ColumnSearchExitsDocumentation": "Le pourcentage de visites où les utilisateurs ont quitté le site après avoir effectué une recherche pour ce mot-clé avec le moteur de recherche de votre site.", + "ColumnSearchResultsCount": "Nombre de résultats de la recherche", + "ColumnSearches": "Recherches", + "ColumnSearchesDocumentation": "Le nombre de visites pour lesquelles une recherche pour ce mot-clé a été effectuée avec le moteur de recherche de votre site web.", + "ColumnSiteSearchKeywords": "Mot-clés uniques", + "ColumnUniqueClicks": "Clics uniques", + "ColumnUniqueClicksDocumentation": "Nombre de visites qui ont impliqué un clic sur ce lien. Si un lien a été cliqué plusieurs fois en une seule visite, il n'est compté qu'une seule fois.", + "ColumnUniqueDownloads": "Téléchargements uniques", + "ColumnUniqueOutlinks": "Liens sortants uniques", + "DownloadsReportDocumentation": "Dans ce rapport vous pouvez voir quels fichiers les utilisateurs ont téléchargés. %s Ce que Matomo compte comme un téléchargement est un clic sur un lien de téléchargement. Le fait que le téléchargement ait été terminé ou non n'est pas connu de Matomo.", + "DownloadsSubcategoryHelp1": "Dans ce rapport, vous pouvez voir quels fichiers vos visiteurs ont téléchargés.", + "DownloadsSubcategoryHelp2": "Ce que Matomo considère comme un téléchargement, c'est le clic sur un lien de téléchargement. Matomo ne sait pas si le téléchargement a été effectué ou non.", + "EntryPageTitles": "Titres de la page d'entrée", + "EntryPageTitlesReportDocumentation": "Ce rapport contient des informations à propos des titres des pages d'entrée qui ont été utilisées durant la période spécifiée.", + "EntryPagesReportDocumentation": "Ce rapport contient des informations à propos des pages d'entrées qui ont été utilisées durant la période spécifiée. Une page d'entrée est la première page qu'un utilisateur voit pendant sa visite. %s Les URLs des pages d'entrée sont affichées dans une structure de dossier.", + "EntryPagesSubcategoryHelp1": "Ce rapport contient des informations sur les pages d'entrée qui ont été utilisées pendant la période spécifiée. Une page d'entrée est la première page qu'un utilisateur consulte pendant sa visite.", + "EntryPagesSubcategoryHelp2": "Les URL des entrées sont affichées sous la forme d'une structure de dossiers.", + "ExitPageTitles": "Titres de la page de sortie", + "ExitPageTitlesReportDocumentation": "Ce rapport contient des infromations à propos des titres de pages de sortie qui ont été utilisées durant la période spécifiée.", + "ExitPagesReportDocumentation": "Ce rapport contient des informations à propos des pages de sortie utilisées durant la période sélectionnée. Une page de sortie est la dernière page de la visite que l'utilisateur voit avant de quitter le site. %s Les Urls de sorties sont affichées dans une structure de dossier.", + "ExitPagesSubcategoryHelp1": "Ce rapport contient des informations sur les pages de sortie qui ont eu lieu pendant la période spécifiée. Une page de sortie est la dernière page qu'un utilisateur consulte pendant sa visite.", + "ExitPagesSubcategoryHelp2": "Les URL de sortie sont affichées sous la forme d'une structure de dossiers.", + "MainMetricsReportDocumentation": "Ce rapport fournit une vue d'ensemble très sommaire des actions réalisées par les visiteurs de votre site web.", + "OneSearch": "1 recherche", + "OutlinkDocumentation": "Un lien sortant est un lien qui mène le visiteur en dehors de votre site web (vers un autre domaine).", + "OutlinksReportDocumentation": "Ce rapport affiche une liste hiérarchique de liens sortants sur lesquels ont cliqué vos visiteurs.", + "OutlinksSubcategoryHelp1": "Ce rapport présente une liste hiérarchique des URL de liens externes qui ont été cliqués par vos visiteurs. Un outlink est un lien qui conduit le visiteur hors de votre site web (vers un autre domaine).", + "PageTitlesReportDocumentation": "Ce rapport contient des informations à propos des titres des pages qui ont été visitées. %1$s Le titre de la page est la balise HTML %2$s que la plupart des navigateurs affichent dans le titre de la fenêtre.", + "PageTitlesSubcategoryHelp1": "Ce rapport contient des informations sur les titres des pages qui ont été visitées.", + "PageTitlesSubcategoryHelp2": "Le titre de la page est la balise HTML &lt;title&gt ; que la plupart des navigateurs affichent dans le titre de leur fenêtre.", + "PageUrls": "URL de la page", + "PageViewsByVisitor": "Nombre de fois où cette page a été vue par ce visiteur au cours de toutes ses visites", + "PagesReportDocumentation": "Ce rapport contient des informations à propos des URLs qui ont été visitées. %s Le tableau est organisé hiérarchiquement, les URLs sont affichées en structure de dossier.", + "PagesSubcategoryHelp1": "Ce rapport contient des informations sur les URL des pages qui ont été visitées.", + "PagesSubcategoryHelp2": "Le tableau est organisé de manière hiérarchique, les URL sont affichées sous forme de structure de dossiers.", + "PagesSubcategoryHelp3": "Utilisez les icônes plus et moins sur la gauche pour naviguer.", + "PluginDescription": "Rapports sur les pages vues et les titres des pages. Vous permet de mesurer le moteur de recherche interne de votre site Web. Suivi automatique des clics sur les liens externes et des téléchargements de fichiers.", + "RevisitedPages": "Pages vues plus d'une fois", + "SearchesSegmentHelp": "Pour sélectionner toutes les visites ayant utilisé la recherche interne du site, utilisez : %s", + "SiteSearchCategories": "Catégories (recherche du site)", + "SiteSearchCategories1": "Ce rapport liste les catégories que les visiteurs ont sélectionnées lorsqu'ils ont effectué une recherche sur votre site web.", + "SiteSearchCategories2": "Par exemple, les sites d'e-commerce ont généralement une sélection de la catégorie afin que les visiteurs puissent affiner leurs recherches aux produits d'une catégories définie.", + "SiteSearchCategory": "Catégories (recherche du site)", + "SiteSearchFollowingPagesDoc": "Quand les visiteurs recherchent sur votre site web, ils sont à la recherche d'une page, d'un produit, d'un contenu ou service en particulier. Ce rapport liste les pages qui ont été le plus cliquées après une recherche interne. En d'autres termes, la liste des pages les plus recherchées par des visiteurs déjà sur votre site.", + "SiteSearchIntro": "Effectuer un suivi des recherches que les visiteurs font sur votre site web est une manière très efficace d'en apprendre plus sur ce que votre audience recherche, cela peut aider à trouver des idées pour du nouveau contenu, un nouveau produit e-commerce dont des clients potentiels pourraient être à la recherche, et de manière générale pour améliorer l'expérience utilisateurs de votre site web.", + "SiteSearchKeyword": "Mot-clé (recherche du site)", + "SiteSearchKeywordCount": "Nombre de mots-clés (Recherche sur le site)", + "SiteSearchKeywordCounts": "Comptage des mots-clés (Recherche sur le site)", + "SiteSearchKeywords": "Mots-clés (recherche du site)", + "SiteSearchKeywordsDocumentation": "Ce rapport liste les mots-clés recherchés dans votre moteur de recherche interne.", + "SiteSearchKeywordsNoResultDocumentation": "Ce rapport liste les mots-clés de recherche qui n'ont retourné aucun résultat : peut-être est-il possible d'améliorer l'algorithme de recherche, ou peut-être que vos visiteurs cherchent du contenu qui n'est pas (encore) sur votre site web ?", + "SiteSearchSubcategoryHelp1": "La section Recherche sur le site montre quels mots-clés les visiteurs utilisent lorsqu'ils recherchent votre site Web. Elle indique également les pages que les utilisateurs consultent après avoir effectué une recherche et les mots-clés de recherche sur site qui ne donnent aucun résultat.", + "SiteSearchSubcategoryHelp2": "Ces rapports peuvent vous donner des idées sur le contenu manquant sur votre site, un aperçu de ce que vos visiteurs recherchent mais ne trouvent pas facilement, et plus encore.", + "SiteSearchSubcategoryHelp3": "Pour en savoir plus, consultez le guide de recherche de sites.", + "SubmenuPageTitles": "Titres des pages", + "SubmenuPagesEntry": "Pages d'entrée", + "SubmenuPagesExit": "Pages de sortie", + "SubmenuSitesearch": "Recherche sur le site", + "TimeSpentInReferringAction": "Temps consacré à l'action de référence", + "WidgetEntryPageTitles": "Titres de pages d'entrée", + "WidgetExitPageTitles": "Titres de pages de sortie", + "WidgetPageTitles": "Titres de page", + "WidgetPageTitlesFollowingSearch": "Titre des pages après une recherche", + "WidgetPageUrlsFollowingSearch": "Pages après une recherche", + "WidgetPagesEntry": "Pages d'entrée", + "WidgetPagesExit": "Pages de sortie", + "WidgetSearchCategories": "Catégories de recherche", + "WidgetSearchKeywords": "Mot-clés recherchés sur le site", + "WidgetSearchNoResultKeywords": "Mots-clés sans résultats" + } +} diff --git a/plugins/Actions/lang/ga.json b/plugins/Actions/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..b3fac7eb35a4aa8985bca09a3f617a8152a1c4fb --- /dev/null +++ b/plugins/Actions/lang/ga.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Cineál Gnímh", + "ActionTypeSegmentHelp": "Cineál gnímh, amhail: %s", + "ActionTypes": "Cineálacha Gníomhaíochta", + "ActionsInVisit": "Gníomhartha ar Cuairt", + "AvgGenerationTimeTooltip": "Meán bunaithe ar %1$s amas(í) %2$s idir %3$s agus%4$s", + "Behaviour": "Iompar", + "ColumnActionURL": "URL an ghnímh", + "ColumnClickedURL": "Cliceáil ar Outlink", + "ColumnClickedURLs": "Naisc Chliceáilte", + "ColumnClicks": "Cad a tharlaíonn", + "ColumnClicksDocumentation": "Líon na n-uaireanta a cliceáladh an nasc seo.", + "ColumnDownloadURL": "Íoslódáil URL", + "ColumnDownloadURLs": "Íosluchtaigh URLanna", + "ColumnEntryPageTitle": "Teideal an Leathanaigh Iontrála", + "ColumnEntryPageURL": "URL an Leathanaigh Iontrála", + "ColumnEntryPageURLs": "URLanna leathanach iontrála", + "ColumnExitPageTitle": "Scoir teideal an leathanaigh", + "ColumnExitPageURL": "Scoir URL an Leathanaigh", + "ColumnExitPageURLs": "URLanna an Leathanaigh Scoir", + "ColumnIdPageview": "Aitheantas amharc leathanaigh", + "ColumnInteractionPosition": "Ionad Idirghníomhaíochta", + "ColumnNoResultKeyword": "Eochairfhocal gan Toradh Cuardaigh", + "ColumnPageName": "Ainm an Leathanaigh", + "ColumnPageURL": "URL an leathanaigh", + "ColumnPageViewPosition": "Ionad Amharc leathanaigh", + "ColumnPagesPerSearch": "Cuardaigh leathanaigh Torthaí", + "ColumnPagesPerSearchDocumentation": "Déanfaidh cuairteoirí cuardach ar do láithreán gréasáin, agus uaireanta cliceáil \"seo chugainn\" chun tuilleadh torthaí a fheiceáil. Is é seo meánlíon na leathanach torthaí cuardaigh a breathnaíodh ar an eochairfhocal seo.", + "ColumnSearchCategory": "Catagóir Chuardaigh", + "ColumnSearchExits": "Bealaí Amach Cuardaigh %%", + "ColumnSearchExitsDocumentation": "Céatadán na gcuairteanna a d'fhág an láithreán gréasáin tar éis cuardach a dhéanamh ar an Eochairfhocal seo ar d'inneall Cuardaigh Láithreáin.", + "ColumnSearchResultsCount": "Líon na dTorthaí Cuardaigh", + "ColumnSearches": "Cuardaigh", + "ColumnSearchesDocumentation": "Líon na gcuairteanna a chuardaigh an eochairfhocal seo ar inneall cuardaigh do láithreán gréasáin.", + "ColumnSiteSearchKeywords": "Eochairfhocail uathúla", + "ColumnUniqueClicks": "Cad a tharlaíonn nuair a tharlaíonn sé", + "ColumnUniqueClicksDocumentation": "Líon na gcuairteanna a raibh cliceáil ar an nasc seo i gceist leo. Má cliceáladh nasc arís agus arís eile le linn cuairte amháin, ní áirítear é ach uair amháin.", + "ColumnUniqueDownloads": "Íoslódálacha Uathúla", + "ColumnUniqueOutlinks": "Naisc Uathúla", + "DownloadsReportDocumentation": "Sa tuarascáil seo, is féidir leat a fheiceáil cé na comhaid a d'íoslódáil do chuairteoirí. %s Is é an rud a chomhaireamh Matomo mar íoslódáil ná cliceáil ar nasc íoslódála. Cibé an raibh an íoslódáil críochnaithe nó nach bhfuil ar eolas ag Matomo.", + "DownloadsSubcategoryHelp1": "Sa tuarascáil seo, is féidir leat a fheiceáil cé na comhaid a d'íoslódáil do chuairteoirí.", + "DownloadsSubcategoryHelp2": "Is é an rud a chomhaireamh Matomo mar íoslódáil ná an cliceáil ar nasc íoslódála. Cibé an raibh an íoslódáil críochnaithe nó nach bhfuil ar eolas ag Matomo.", + "EntryPageTitles": "Teidil an leathanaigh iontrála", + "EntryPageTitlesReportDocumentation": "Tá faisnéis sa tuarascáil seo faoi theidil na leathanach iontrála a úsáideadh le linn na tréimhse sonraithe.", + "EntryPagesReportDocumentation": "Tá faisnéis sa tuarascáil seo faoi na leathanaigh iontrála a úsáideadh le linn na tréimhse sonraithe. Is é leathanach iontrála an chéad leathanach a fhéachann úsáideoir le linn a gcuairte. %s Taispeántar URLanna na hiontrála mar struchtúr fillteáin.", + "EntryPagesSubcategoryHelp1": "Tá faisnéis sa tuarascáil seo faoi na leathanaigh iontrála a úsáideadh le linn na tréimhse sonraithe. Is é leathanach iontrála an chéad leathanach a fhéachann úsáideoir le linn a gcuairte.", + "EntryPagesSubcategoryHelp2": "Taispeántar URLanna na hiontrála mar struchtúr fillteáin.", + "ExitPageTitles": "Scoir teidil an leathanaigh", + "ExitPageTitlesReportDocumentation": "Tá faisnéis sa tuarascáil seo faoi theidil na leathanach imeachta a tharla le linn na tréimhse sonraithe.", + "ExitPagesReportDocumentation": "Tá faisnéis sa tuarascáil seo faoi na leathanaigh imeachta a tharla le linn na tréimhse sonraithe. Is é leathanach imeachta an leathanach deireanach a fhéachann úsáideoir le linn a gcuairte. %s Taispeántar na URLanna scoir mar struchtúr fillteáin.", + "ExitPagesSubcategoryHelp1": "Tá faisnéis sa tuarascáil seo faoi na leathanaigh imeachta a tharla le linn na tréimhse sonraithe. Is é leathanach imeachta an leathanach deireanach a fhéachann úsáideoir le linn a gcuairte.", + "ExitPagesSubcategoryHelp2": "Taispeántar na URLanna scoir mar struchtúr fillteáin.", + "MainMetricsReportDocumentation": "Soláthraíonn an tuarascáil seo forbhreathnú an-bhunúsach ar na gníomhartha a ghlacann do chuairteoirí ar do láithreán gréasáin.", + "OneSearch": "1 cuardach", + "OutlinkDocumentation": "Is nasc é outlink a threoraíonn an cuairteoir ar shiúl ó do láithreán gréasáin (go fearann eile).", + "OutlinksReportDocumentation": "Taispeánann an tuarascáil seo liosta ordlathach de URLanna outlink a chliceáil do chuairteoirí.", + "OutlinksSubcategoryHelp1": "Taispeánann an tuarascáil seo liosta ordlathach de URLanna outlink a chliceáil do chuairteoirí. Is nasc é outlink a threoraíonn an cuairteoir ar shiúl ó do láithreán gréasáin (go fearann eile).", + "PageTitlesReportDocumentation": "Tá eolas sa tuarascáil seo faoi theidil na leathanach ar tugadh cuairt orthu. %1$s Is é teideal an leathanaigh an Chlib HTML %2$s a thaispeánann formhór na mbrabhsálaithe ina dteideal fuinneoige.", + "PageTitlesSubcategoryHelp1": "Tá eolas sa tuarascáil seo faoi theidil na leathanach ar tugadh cuairt orthu.", + "PageTitlesSubcategoryHelp2": "Is é teideal an leathanaigh an Chlib HTML &lt;title&gt; a thaispeánann formhór na mbrabhsálaithe ina dteideal fuinneoige.", + "PageUrls": "URLanna leathanaigh", + "PageViewsByVisitor": "Líon na n-uaireanta a bhreathnaigh an cuairteoir seo ar an leathanach seo i ngach cuairt", + "PagesReportDocumentation": "Tá faisnéis sa tuarascáil seo faoi URLanna an leathanaigh ar tugadh cuairt orthu. %s Eagraítear an tábla go hordlathach, taispeántar na URLanna mar struchtúr fillteáin.", + "PagesSubcategoryHelp1": "Tá faisnéis sa tuarascáil seo faoi URLanna an leathanaigh ar tugadh cuairt orthu.", + "PagesSubcategoryHelp2": "Tá an tábla eagraithe ordlathach, taispeántar na URLanna mar struchtúr fillteáin.", + "PagesSubcategoryHelp3": "Bain úsáid as na deilbhíní móide agus lúide ar chlé chun nascleanúint a dhéanamh.", + "PluginDescription": "Tuairiscí faoi thuairimí an leathanaigh agus teidil na leathanach. Ligeann duit inneall cuardaigh do láithreán gréasáin inmheánach a thomhas. Rianta go huathoibríoch cad a tharlaíonn ar naisc sheachtracha agus íoslódálacha comhad.", + "RevisitedPages": "Leathanaigh a breathnaíodh níos mó ná uair amháin", + "SearchesSegmentHelp": "Chun na cuairteanna uile a d'úsáid Cuardach Suímh inmheánach a roghnú, bain úsáid as: %s", + "SiteSearchCategories": "Catagóirí (Cuardach Láithreáin)", + "SiteSearchCategories1": "Liostaítear sa tuarascáil seo na Catagóirí a roghnaigh cuairteoirí nuair a rinne siad Cuardach ar do láithreán gréasáin.", + "SiteSearchCategories2": "Mar shampla, de ghnáth bíonn roghnóir \"Catagóir\" ag láithreáin ghréasáin ríomhthráchtála ionas gur féidir le cuairteoirí a gcuardach a shrianadh do gach táirge i gCatagóir ar leith.", + "SiteSearchCategory": "Catagóir (Cuardach Láithreáin)", + "SiteSearchFollowingPagesDoc": "Nuair a dhéanann cuairteoirí cuardach ar do láithreán gréasáin, tá leathanach, ábhar, táirge nó seirbhís ar leith á lorg acu. Liostaítear sa tuarascáil seo na leathanaigh is mó a ndearnadh cliceáil orthu tar éis cuardach inmheánach. Is é sin le rá, liosta na leathanach is mó a ndearna cuairteoirí cuardach orthu cheana féin ar do láithreán gréasáin.", + "SiteSearchIntro": "Is bealach an-éifeachtach é cuardaigh rianaithe a dhéanann cuairteoirí ar do láithreán gréasáin chun níos mó a fhoghlaim faoi na rudaí atá á lorg ag do lucht féachana, is féidir leis cabhrú le smaointe a aimsiú le haghaidh ábhar nua, táirgí ríomhthráchtála nua a d'fhéadfadh custaiméirí ionchasacha a chuardach, agus go ginearálta eispéireas na gcuairteoirí ar do láithreán gréasáin a fheabhsú.", + "SiteSearchKeyword": "Eochairfhocal (Cuardach Suímh)", + "SiteSearchKeywordCount": "Líon na n-eochairfhocal (Cuardach Láithreáin)", + "SiteSearchKeywordCounts": "Líon eochairfhocal (Cuardach Láithreáin)", + "SiteSearchKeywords": "Eochairfhocail (Cuardach Suímh)", + "SiteSearchKeywordsDocumentation": "Liostaítear sa tuarascáil seo na hEochairfhocail Chuardaigh a chuardaigh cuairteoirí ar d'Inneall Cuardaigh inmheánach.", + "SiteSearchKeywordsNoResultDocumentation": "Liostaítear sa tuarascáil seo na hEochairfhocail Chuardaigh nár thug aon toradh Cuardaigh ar ais: b'fhéidir gur féidir algartam an innill chuardaigh a fheabhsú, nó b'fhéidir go bhfuil do chuairteoirí ag lorg ábhar nach bhfuil (fós) ar do láithreán gréasáin?", + "SiteSearchSubcategoryHelp1": "Taispeánann an rannán Cuardach Láithreáin na heochairfhocail a úsáideann cuairteoirí agus iad ag cuardach do láithreán gréasáin. Taispeánann sé freisin na leathanaigh a bhféachann úsáideoirí orthu tar éis cuardach a dhéanamh agus nach dtugann eochairfhocail chuardaigh ar an láthair aon torthaí ar chor ar bith.", + "SiteSearchSubcategoryHelp2": "Is féidir leis na tuarascálacha seo smaointe a thabhairt duit faoi ábhar atá ar iarraidh ar do shuíomh, léargas ar an méid atá á lorg ag do chuairteoirí ach ní féidir leo teacht go héasca, agus níos mó.", + "SiteSearchSubcategoryHelp3": "Foghlaim níos mó sa treoir Cuardaigh Láithreáin.", + "SubmenuPageTitles": "Teidil an leathanaigh", + "SubmenuPagesEntry": "Leathanaigh iontrála", + "SubmenuPagesExit": "Leathanaigh scoir", + "SubmenuSitesearch": "Cuardach Láithreáin", + "TimeSpentInReferringAction": "An tAm a Chaitear i nGníomh a Tharchur", + "WidgetEntryPageTitles": "Teidil leathanach iontrála", + "WidgetExitPageTitles": "Scoir Teidil Leathanaigh", + "WidgetPageTitles": "Teidil an Leathanaigh", + "WidgetPageTitlesFollowingSearch": "Teidil leathanaigh tar éis cuardach láithreáin", + "WidgetPageUrlsFollowingSearch": "Leathanaigh Tar éis Cuardach Láithreáin", + "WidgetPagesEntry": "Leathanaigh Iontrála", + "WidgetPagesExit": "Scoir de Leathanaigh", + "WidgetSearchCategories": "Catagóirí Cuardaigh", + "WidgetSearchKeywords": "Eochairfhocail Chuardaigh an tSuímh", + "WidgetSearchNoResultKeywords": "Cuardaigh Eochairfhocail gan Torthaí" + } +} diff --git a/plugins/Actions/lang/gl.json b/plugins/Actions/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..ea95a5220ff62864d5df3d017be3a28130c378f5 --- /dev/null +++ b/plugins/Actions/lang/gl.json @@ -0,0 +1,7 @@ +{ + "Actions": { + "ColumnPageName": "Nome da páxina", + "ColumnPageURL": "URL da páxina", + "ColumnSearches": "Buscas" + } +} diff --git a/plugins/Actions/lang/gu.json b/plugins/Actions/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..2a621d5e7886b44f1f4aa1dd10632d179e4e80bc --- /dev/null +++ b/plugins/Actions/lang/gu.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "ક્રિયા પ્રકાર", + "ActionTypes": "ક્રિયા પ્રકારો", + "ActionsInVisit": "મુલાકાતમાં ક્રિયાઓ", + "AvgGenerationTimeTooltip": "%3$s અને %4$s વચ્ચેના %1$s હિટ(s) %2$s પર આધારિત સરેરાશ", + "Behaviour": "વર્તન", + "ColumnActionURL": "ક્રિયા URL", + "ColumnClickedURL": "આઉટલિંક ક્લિક કર્યું", + "ColumnClickedURLs": "ક્લિક કરેલા આઉટલિંક્સ", + "ColumnClicks": "ક્લિક્સ", + "ColumnClicksDocumentation": "આ લિંકને આટલી વાર ક્લિક કરવામાં આવી હતી.", + "ColumnDownloadURL": "યુ.આર.એલ ડાઉનલોડ કરો", + "ColumnDownloadURLs": "ડાઉનલોડ URLs", + "ColumnEntryPageTitle": "પ્રવેશ પેજનું શીર્ષક", + "ColumnEntryPageURL": "પ્રવેશ પેજનું યુ.આર.એલ", + "ColumnEntryPageURLs": "પ્રવેશ પૃષ્ઠ URLs", + "ColumnExitPageTitle": "એક્ઝીટ પેજ નું શીર્ષક", + "ColumnExitPageURL": "એક્ઝીટ પેજ નું યુ.આર.એલ", + "ColumnExitPageURLs": "નિર્ગમ પૃષ્ઠ URLs", + "ColumnIdPageview": "પૃષ્ઠ દૃશ્ય ID", + "ColumnInteractionPosition": "ક્રિયાપ્રતિક્રિયા સ્થાન", + "ColumnNoResultKeyword": "શોધ પરિણામ વગર નો કીવર્ડ", + "ColumnPageName": "પેજनु નામ", + "ColumnPageURL": "પેજનું યુ.આર.એલ", + "ColumnPageViewPosition": "પૃષ્ઠ દૃશ્ય સ્થાન", + "ColumnPagesPerSearch": "શોધ પરિણામો ના પાના", + "ColumnPagesPerSearchDocumentation": "વિઝીટર્સ તમારી વેબસાઇટ પર સર્ચ કરશે અને ક્યારેક વધુ પરિણામો જોવા \"નેક્ષ્ટ\" પર ક્લિક કરશે. આ એ કીવર્ડ માટે જોવાયા પૃષ્ઠોની સરેરાશ નંબર છે.", + "ColumnSearchCategory": "સર્ચ કેટેગરી", + "ColumnSearchExits": "%% શોધ નિર્ગમ", + "ColumnSearchExitsDocumentation": "તમારી સાઇટ સર્ચ એન્જિન પર આ કિવર્ડ માટે શોધ પછી વેબસાઇટ એક્ઝીટ કરી હોય તેવી મુલાકાતો ટકા માં", + "ColumnSearchResultsCount": "શોધ પરિણામોની ગણતરી", + "ColumnSearches": "શોધો", + "ColumnSearchesDocumentation": "તમારી વેબસાઇટ ના શોધ એન્જિન પર આ કીવર્ડ માટે શોધ કરેલ મુલાકાતો ની સંખ્યા.", + "ColumnSiteSearchKeywords": "અનન્ય કીવર્ડ્સ", + "ColumnUniqueClicks": "વિશિષ્ટ ક્લિક્સ", + "ColumnUniqueClicksDocumentation": "આ લિંક પર ક્લિક કરતી મુલાકાતોની સંખ્યા. જો એક મુલાકાત દરમિયાન કોઈ લિંકને ઘણી વખત ક્લિક કરવામાં આવી હોય, તો તે માત્ર એક જ વાર ગણવામાં આવે છે.", + "ColumnUniqueDownloads": "અનન્ય ડાઉનલોડ", + "ColumnUniqueOutlinks": "અનન્ય આઉટલિંક્સ", + "DownloadsReportDocumentation": "આ રિપોર્ટમાં, તમે જોઈ શકો છો કે તમારા મુલાકાતીઓએ કઈ ફાઇલો ડાઉનલોડ કરી છે. %s જે Matomo ડાઉનલોડ તરીકે ગણે છે તે ડાઉનલોડ લિંક પર ક્લિક છે. ડાઉનલોડ પૂર્ણ થયું કે નહીં તે Matomo ને ખબર નથી.", + "DownloadsSubcategoryHelp1": "આ અહેવાલમાં, તમે જોઈ શકો છો કે તમારા મુલાકાતીઓએ કયા ફાઇલો ડાઉનલોડ કર્યા છે.", + "DownloadsSubcategoryHelp2": "Matomo જેને ડાઉનલોડ તરીકે ગણે છે તે ડાઉનલોડ લિંક પર ક્લિક છે. ડાઉનલોડ પૂર્ણ થયું કે નહીં તે Matomo ને ખબર નથી.", + "EntryPageTitles": "પ્રવેશ પૃષ્ઠ શીર્ષકો", + "EntryPageTitlesReportDocumentation": "આ અહેવાલમાં તેમના વિશેષ સમયગાળો દરમિયાન વાપરાયેલા પ્રવેશ પૃષ્ઠોના શીર્ષકોની માહિતી શામેલ છે.", + "EntryPagesReportDocumentation": "આ અહેવાલમાં તેવા પ્રવેશ પૃષ્ઠોની માહિતી છે જે નિર્દિષ્ટ સમયગાળા દરમિયાન વપરાયેલ છે. પ્રવેશ પૃષ્ઠ એવું પૃષ્ઠ છે જે વપરાશકર્તા તેમની મુલાકાત દરમિયાન પ્રથમ જોયે છે. %s પ્રવેશ URLs ફોલ્ડર સંરચના તરીકે પ્રદર્શિત કરવામાં આવે છે.", + "EntryPagesSubcategoryHelp1": "આ અહેવાલમાં ઉલ્લેખિત સમયગાળા દરમિયાન ઉપયોગમાં લેવાતા એન્ટ્રી પૃષ્ઠો વિશેની માહિતી શામેલ છે. પ્રવેશ પૃષ્ઠ એ પ્રથમ પૃષ્ઠ છે જે વપરાશકર્તા તેમની મુલાકાત દરમિયાન જુએ છે.", + "EntryPagesSubcategoryHelp2": "એન્ટ્રી URL ફોલ્ડર સ્ટ્રક્ચર તરીકે પ્રદર્શિત થાય છે.", + "ExitPageTitles": "નિર્ગમ પૃષ્ઠ શીર્ષકો", + "ExitPageTitlesReportDocumentation": "આ અહેવાલમાં નિર્દિષ્ટ સમયગાળો દરમિયાન થયેલા નિર્ગમ પૃષ્ઠોના શીર્ષકો વિશેની માહિતી શામેલ છે.", + "ExitPagesReportDocumentation": "આ અહેવાલમાં નિર્દિષ્ટ સમયગાળો દરમિયાન થયેલા નિર્ગમ પૃષ્ઠો વિશેની માહિતી છે. નિર્ગમ પૃષ્ઠ એ એવું છે કે જે વપરાશકર્તા તેમની મુલાકાત દરમિયાન છેલ્લે જોવે છે. %s નિર્ગમ URLs એક ફોલ્ડર સંરચના તરીકે પ્રદર્શિત કરવામાં આવે છે.", + "ExitPagesSubcategoryHelp1": "આ રિપોર્ટમાં નિર્દિષ્ટ સમયગાળા દરમિયાન બહાર નીકળેલા પૃષ્ઠો વિશેની માહિતી શામેલ છે. એક્ઝિટ પેજ એ છેલ્લું પેજ છે જે વપરાશકર્તા તેમની મુલાકાત દરમિયાન જુએ છે.", + "ExitPagesSubcategoryHelp2": "એક્ઝિટ URL ફોલ્ડર સ્ટ્રક્ચર તરીકે પ્રદર્શિત થાય છે.", + "MainMetricsReportDocumentation": "આ અહેવાલ તમારી વેબસાઇટ પર તમારા મુલાકાતીઓ શું ક્રિયાઓ લે છે તેની ખૂબ મૂળભૂત ઝાંખી આપે છે.", + "OneSearch": "1 શોધ", + "OutlinkDocumentation": "આઉટલિંક એ એવું લિંક છે જે મુલાકાતીને તમારી વેબસાઇટથી દૂર (અન્ય ડોમેન પર) લઈ જાય છે.", + "OutlinksReportDocumentation": "આ અહેવાલ તમારા મુલાકાતીઓ દ્વારા ક્લિક કરેલા આઉટલિંક URLs ની હાયરાર્કીકલ યાદી બતાવે છે.", + "OutlinksSubcategoryHelp1": "આ રિપોર્ટ તમારા મુલાકાતીઓ દ્વારા ક્લિક કરાયેલા આઉટલિંક URL ની શ્રેણીબદ્ધ સૂચિ દર્શાવે છે. આઉટલિંક એ એક લિંક છે જે મુલાકાતીને તમારી વેબસાઇટથી દૂર લઈ જાય છે (બીજા ડોમેન પર).", + "PageTitlesReportDocumentation": "આ અહેવાલમાં મુલાકાત લીધેલ પૃષ્ઠોના શીર્ષકો વિશેની માહિતી છે. %1$s પૃષ્ઠ શીર્ષક એ HTML %2$s ટેગ છે જે મોટાભાગના બ્રાઉઝર તેમના વિન્ડો શીર્ષકમાં દર્શાવે છે.", + "PageTitlesSubcategoryHelp1": "આ અહેવાલમાં મુલાકાત લીધેલ પૃષ્ઠોના શીર્ષકો વિશેની માહિતી છે.", + "PageTitlesSubcategoryHelp2": "પેજ શીર્ષક એ HTML &lt;title&gt; ટેગ છે જેને વધું વધુ બ્રાઉઝરો તેમના વિન્ડો શીર્ષકમાં બતાવે છે.", + "PageUrls": "પૃષ્ઠ URLs", + "PageViewsByVisitor": "આ મુલાકાતીએ બધી મુલાકાતો દરમિયાન આ પૃષ્ઠને કેટલી વાર જોયું છે, તેની સંખ્યા", + "PagesReportDocumentation": "આ અહેવાલમાં મુલાકાત લીધેલ પૃષ્ઠ URL વિશેની માહિતી છે. %s કોષ્ટક અધિક્રમિક રીતે ગોઠવાયેલ છે, URLs ફોલ્ડર સ્ટ્રક્ચર તરીકે પ્રદર્શિત થાય છે.", + "PagesSubcategoryHelp1": "આ અહેવાલમાં તે પૃષ્ઠ URL વિશેની માહિતી છે જેની મુલાકાત લીધી છે.", + "PagesSubcategoryHelp2": "કોષ્ટક હાયરાર્કીકલી ગોઠવાયેલ છે, URL ફોલ્ડર માળખા તરીકે પ્રદર્શિત થાય છે.", + "PagesSubcategoryHelp3": "નેવિગેટ કરવા માટે ડાબી બાજુના પ્લસ અને માઈનસ ચિહ્નોનો ઉપયોગ કરો.", + "PluginDescription": "પૃષ્ઠ દૃશ્યો અને પૃષ્ઠ શીર્ષકો વિશેના અહેવાલો. તમને તમારી આંતરિક વેબસાઇટના સર્ચ એન્જિનને માપવા દે છે. બાહ્ય લિંક્સ અને ફાઇલ ડાઉનલોડ્સ પરના ક્લિક્સને આપમેળે ટ્રૅક કરે છે.", + "RevisitedPages": "એક કરતાં વધુ વખત જોવાયેલા પૃષ્ઠો", + "SiteSearchCategories": "વર્ગો (સાઇટ શોધ)", + "SiteSearchCategories1": "આ અહેવાલમાં તે શ્રેણીઓ છે જે મુલાકાતીઓએ તમારી વેબસાઇટ પર શોધ કરતાં વખતે પસંદ કરી.", + "SiteSearchCategories2": "ઉદાહરણ તરીકે, ઈકોમર્સ વેબસાઇટ્સમાં સામાન્ય રીતે \"શ્રેણી\" પસંદગી હોય છે જેથી મુલાકાતીઓ તેમની શોધની મર્યાદાઓ વિશેષ શ્રેણીમાં બધા ઉત્પાદનો પર મર્યાદિત કરી શકે છે.", + "SiteSearchCategory": "વર્ગ (સાઇટ શોધ)", + "SiteSearchFollowingPagesDoc": "જ્યારે મુલાકાતીઓ તમારી વેબસાઇટ પર શોધ કરે છે, ત્યારે તેઓ ચોક્કસ પૃષ્ઠ, સામગ્રી, ઉત્પાદન અથવા સેવા શોધી રહ્યાં છે. આ અહેવાલમાં આંતરિક શોધ પછી સૌથી વધુ ક્લિક કરાયેલા પૃષ્ઠોની સૂચિ છે. બીજા શબ્દોમાં કહીએ તો, તમારી વેબસાઇટ પર પહેલેથી જ મુલાકાતીઓ દ્વારા સૌથી વધુ શોધાયેલ પૃષ્ઠોની સૂચિ.", + "SiteSearchIntro": "તમારી વેબસાઇટ પર મુલાકાતીઓ દ્વારા કરવામાં આવતી શોધની ટ્રેકિંગ એ તમારા પ્રેક્ષકો શું શોધી રહ્યાં છે તે વધુ જાણવાનો ખૂબ કાર્યક્ષમ માર્ગ છે, તે નવી સામગ્રી માટે વિચારો શોધવામાં, નવી ઈકોમર્સ ઉત્પાદનો જે સંભવિત ગ્રાહકો શોધી રહ્યાં હોઈ શકે છે, અને સામાન્ય રીતે તમારી વેબસાઇટ પર મુલાકાતીઓનું અનુભવ સુધારવામાં મદદ કરી શકે છે.", + "SiteSearchKeyword": "કીવર્ડ (સાઇટ શોધ)", + "SiteSearchKeywordCount": "કીવર્ડ ગણતરી (સાઇટ શોધ)", + "SiteSearchKeywordCounts": "કીવર્ડ ગણતરીઓ (સાઇટ શોધ)", + "SiteSearchKeywords": "કીવર્ડ્સ (સાઇટ શોધ)", + "SiteSearchKeywordsDocumentation": "આ અહેવાલમાં તમારા આંતરિક શોધ એન્જિન પર મુલાકાતીઓ દ્વારા શોધવામાં આવેલા શોધ કીવર્ડ્સની યાદી છે.", + "SiteSearchKeywordsNoResultDocumentation": "આ અહેવાલમાં એવા શોધ કીવર્ડ્સની યાદી છે જેને કોઈ પણ શોધ પરિણામ મળ્યો નથી: શાયદ શોધ એન્જિન એલ્ગોરિદમને સુધારવાની જરૂર હોઈ શકે છે, અથવા શાયદ તમારા મુલાકાતીઓ એવી સામગ્રી માટે શોધી રહ્યાં છે જે તમારી વેબસાઇટ પર (હજુ સુધી) નથી?", + "SiteSearchSubcategoryHelp1": "સાઇટ શોધ વિભાગ બતાવે છે કે મુલાકાતીઓ તમારી વેબસાઇટ શોધતી વખતે કયા કીવર્ડ્સનો ઉપયોગ કરે છે. તે એ પણ દર્શાવે છે કે વપરાશકર્તાઓ શોધ કર્યા પછી કયા પૃષ્ઠો જુએ છે અને કયા ઑન-સાઇટ સર્ચ કીવર્ડ્સ કોઈ પરિણામ આપતા નથી.", + "SiteSearchSubcategoryHelp2": "આ અહેવાલો તમને તમારી સાઇટ પર ગુમ થયેલ સામગ્રી વિશેના વિચારો, તમારા મુલાકાતીઓ શું શોધી રહ્યા છે પરંતુ સરળતાથી શોધી શકતા નથી તેની સમજ આપી શકે છે અને વધુ.", + "SiteSearchSubcategoryHelp3": "સાઇટ શોધ માર્ગદર્શિકામાં વધુ શીખો.", + "SubmenuPageTitles": "પૃષ્ઠ શીર્ષકો", + "SubmenuPagesEntry": "પ્રવેશ પૃષ્ઠો", + "SubmenuPagesExit": "નિર્ગમ પૃષ્ઠો", + "SubmenuSitesearch": "સાઇટ શોધ", + "TimeSpentInReferringAction": "સંદર્ભ ક્રિયામાં વિતાવેલો સમય", + "WidgetEntryPageTitles": "પ્રવેશ પૃષ્ઠ શીર્ષકો", + "WidgetExitPageTitles": "નિર્ગમ પૃષ્ઠ શીર્ષકો", + "WidgetPageTitles": "પૃષ્ઠ શીર્ષકો", + "WidgetPageTitlesFollowingSearch": "સાઇટ શોધ પછીના પૃષ્ઠ શીર્ષકો", + "WidgetPageUrlsFollowingSearch": "સાઇટ શોધ પછીના પૃષ્ઠો", + "WidgetPagesEntry": "પ્રવેશ પૃષ્ઠો", + "WidgetPagesExit": "નિર્ગમ પૃષ્ઠો", + "WidgetSearchCategories": "શોધ વર્ગો", + "WidgetSearchKeywords": "સાઇટ શોધ કીવર્ડ્સ", + "WidgetSearchNoResultKeywords": "કોઈ પરિણામો વગરના શોધ કીવર્ડ્સ" + } +} diff --git a/plugins/Actions/lang/he.json b/plugins/Actions/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..69202212c2ebb2a2b05dae0ece23765aeb4210f8 --- /dev/null +++ b/plugins/Actions/lang/he.json @@ -0,0 +1,55 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "ממוצע מבוסס על %1$s מופע(ים) %2$s בין %3$s ו-%4$s", + "ColumnClicks": "לחיצות", + "ColumnClicksDocumentation": "מספר הפעמים שקישור זה נלחץ", + "ColumnDownloadURL": "הורדת כתובת", + "ColumnEntryPageTitle": "כותרת עמוד כניסה", + "ColumnEntryPageURL": "כתובת עמוד כניסה", + "ColumnExitPageTitle": "כותרת עמוד יציאה", + "ColumnExitPageURL": "כתובת עמוד יציאה", + "ColumnNoResultKeyword": "מילות מפתח ללא תוצאות חיפוש", + "ColumnPageName": "שם העמוד", + "ColumnPageURL": "כתובת העמוד", + "ColumnPagesPerSearch": "עמודי תוצאות חיפוש", + "ColumnPagesPerSearchDocumentation": "מבקרים יחפשו באתרך, ולפעמים ילחצו ״קדימה״ לצפייה בתוצאות נוספות. זהו ממוצע עמודי תוצאות החיפוש שנצפו למילת מפתח זו.", + "ColumnSearchCategory": "קטגורית חיפוש", + "ColumnSearchExitsDocumentation": "אחוז הביקורים שעזבו את האתר אחרי חיפוש מילת מפתח זו במנוע החיפוש של אתרך.", + "ColumnSearchResultsCount": "מספר תוצאות חיפוש", + "ColumnSearches": "חיפושים", + "ColumnSearchesDocumentation": "מספר הביקורים שחיפשו אחר מילת מפתח זו במנוע החיפוש של אתרך.", + "ColumnSiteSearchKeywords": "מילות מפתח ייחודיות", + "ColumnUniqueClicks": "לחיצות ייחודיות", + "ColumnUniqueClicksDocumentation": "מספר הביקורים שכללו לחיצה על קישור זה. אם קישור נלחץ יותר מפעם אחת במהלך ביקור, הוא יספר פעם אחת בלבד.", + "ColumnUniqueDownloads": "הורדות ייחודיות", + "ColumnUniqueOutlinks": "קישורים יוצאים ייחודיים", + "DownloadsReportDocumentation": "בדו״ח זה, ניתן לראות אילו קבצים מבקרים הורידו. %s מה ש-Matomo סופרת כהורדה זה לחיצה על קישור ההורדה. בין אם ההורדה הסתיימה או לא - את זה Matomo לא יודעת.", + "EntryPageTitles": "כותרות עמוד כניסה", + "EntryPageTitlesReportDocumentation": "דו״ח זה מכיל מידע אודות הכותרות של עמודי כניסה שהיו בשימוש במהלך התקופה המוגדרת.", + "ExitPageTitles": "כותרות עמוד יציאה", + "ExitPageTitlesReportDocumentation": "דו״ח זה מכיל מידע אודות כותרות של עמודי יציאה שהתקיימו במהלך התקופה המוגדרת.", + "OneSearch": "חיפוש 1", + "OutlinkDocumentation": "קישור יוצא הוא קישור שמוביל את המשתמש מחוץ לאתרך (לדומיין אחר).", + "OutlinksReportDocumentation": "דו״ח זה מציג רשימה היררכית של כתובות שנלחצו על ידי משתמשי אתרך.", + "PageTitlesReportDocumentation": "דו״ח זה מכיל מידע אודות כותרות העמודים שבוקרו. %1$s כותרת העמוד היא תגית HTML %2$s שרוב הדפדפנים מציגים בכותרת החלון שלהם.", + "PageUrls": "כתובות עמוד", + "PagesReportDocumentation": "דו״ח זה מכיל מידע אודות כתובת עמודים שבוקרו. %s הטבלה מאורגנת באופן היררכי, הכתובות מוצגות כמבנה תיקיות.", + "PagesSubcategoryHelp3": "השתמש בצלמיות הפלוס והמינוס בצד שמאל על מהת לנווט.", + "SiteSearchCategories1": "דו״ח זה מציג רשימה של קטגוריות שמבקרים בחרו כאשר חיפשו באתרך.", + "SiteSearchCategories2": "לדוגמה, אתרי מסחר מקוונים מציגים בד״כ בחירת ״קטגוריה״ כדי שהמבקרים יוכלו לצמצם את תוצאות החיפוש למוצרים בקטגוריה מסויימת.", + "SiteSearchKeyword": "מילת מפתח (חיפוש באתר)", + "SiteSearchKeywordsDocumentation": "דו״ח זה מציג רשימה של מילות חיפוש שמבקרים חיפשו אחריהן במנוע החיפוש הפנימי של אתרך.", + "SubmenuPageTitles": "כותרות עמוד", + "SubmenuPagesEntry": "עמודי כניסה", + "SubmenuPagesExit": "עמודי יציאה", + "SubmenuSitesearch": "חיפוש באתר", + "WidgetEntryPageTitles": "כותרות עמוד כניסה", + "WidgetExitPageTitles": "כותרות עמוד יציאה", + "WidgetPageTitles": "כותרות עמוד", + "WidgetPagesEntry": "עמודי כניסה", + "WidgetPagesExit": "עמודי יציאה", + "WidgetSearchCategories": "קטגוריות חיפוש", + "WidgetSearchKeywords": "מילות חיפוש באתר", + "WidgetSearchNoResultKeywords": "מילות חיפוש ללא תוצאות" + } +} diff --git a/plugins/Actions/lang/hi.json b/plugins/Actions/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..a8dec77c1b68110337b8e3fa468d09e5ae6c69c1 --- /dev/null +++ b/plugins/Actions/lang/hi.json @@ -0,0 +1,59 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "%3$s और %4$s के बीच %1$s हिट(ओं) %2$s पर आधारित औसत", + "ColumnClicks": "क्लिकों", + "ColumnClicksDocumentation": "इस लिंक पर इतनी बार क्लिक किया गया था", + "ColumnDownloadURL": "यू.आर.एल डाउनलोड करें", + "ColumnEntryPageTitle": "प्रवेश पेज का शीर्षक", + "ColumnEntryPageURL": "प्रवेश पृष्ठ का यू.आर.एल", + "ColumnExitPageTitle": "एक्जिट पेज का शीर्षक", + "ColumnExitPageURL": "एक्जिट पेज का यू.आ.रएल", + "ColumnNoResultKeyword": "कीवर्ड बगैर सर्च रिजल्ट के", + "ColumnPageName": "पेज का नाम", + "ColumnPageURL": "पृष्ठ का यू.आर.एल", + "ColumnPagesPerSearch": "खोज परिणाम के पन्नों", + "ColumnPagesPerSearchDocumentation": "विजिटर्स आपकी वेबसाइट पर सर्च करेंगे और कभी कभी अधिक रिजल्ट्स देखने के लिए \"नेक्स्ट\" पर क्लिक करेंगे। यह इस कीवर्ड के लिए देखी खोज परिणाम पृष्ठों की औसत संख्या है। यह इस कीवर्ड के लिए देखे गए सर्च रिजल्ट पृष्ठों की औसत संख्या है।", + "ColumnSearchCategory": "सर्च श्रेणी", + "ColumnSearchExitsDocumentation": "आपके साइट सर्च इंजन पर इस कीवर्ड के लिए खोज करने के बाद वेबसाइट को छोड़ दिया हो ऐसी विजिट्स के प्रतिशत।", + "ColumnSearchResultsCount": "खोज परिणाम गणना", + "ColumnSearches": "खोजें", + "ColumnSearchesDocumentation": "अपनी वेबसाइट की खोज इंजन पर इस कीवर्ड के लिए खोज की है कि यात्राओं की संख्या.", + "ColumnSiteSearchKeywords": "अद्वितीय कीवर्ड", + "ColumnUniqueClicks": "विशिष्ट क्लिक", + "ColumnUniqueClicksDocumentation": "दिए गए लिंक पर क्लिक करने वाले विज़िट्स की संख्या। यदि एक विज़िट में किसी भी लिंक पर अनेकों बार क्लिक किया गया हो, तो उसे एक ही बार गिना जाता है।", + "ColumnUniqueDownloads": "विशिष्ट डाउनलोड", + "ColumnUniqueOutlinks": "विशिष्ट आउटलिंक", + "DownloadsReportDocumentation": "यह रिपोर्ट आपकी वेबसाइट पर आने वाले व्यक्तियों द्वारा डाउनलोड की गयी फाइलों को दर्शाती है। जब कोई व्यक्ति किसी डाउनलोड लिंक %s पर क्लिक करता है, तो पिविक उसे डाउनलोड की दृष्टि से देखता है। डाउनलोड सही रूप से हुआ या नहीं, इस बात की जानकारी पिविक को नहीं होती।", + "EntryPageTitles": "एंट्री पेज खिताब", + "EntryPageTitlesReportDocumentation": "इस रिपोर्ट में निर्दिष्ट अवधि के दौरान इस्तेमाल किया गया है कि प्रवेश पृष्ठों के शीर्षक के बारे में जानकारी शामिल हैं.", + "ExitPageTitles": "पृष्ठ शीर्षक से बाहर निकलें", + "ExitPageTitlesReportDocumentation": "इस रिपोर्ट में निर्दिष्ट अवधि के दौरान हुई कि निकास पृष्ठों के शीर्षक के बारे में जानकारी शामिल हैं.", + "OutlinkDocumentation": "आउटलिंक उस लिंक को कहा जाता है जो आपकी वेबसाइट पर आये व्यक्तियों को किसी अन्य वेबसाइट पर ले जाता है।", + "OutlinksReportDocumentation": "इस रिपोर्ट में अपने आगंतुकों द्वारा क्लिक किया गया है कि बाहर कड़ी यूआरएल की एक पदानुक्रमित सूची दिखाता है.", + "PageTitlesReportDocumentation": "इस रिपोर्ट का दौरा किया गया है कि पृष्ठों के शीर्षक के बारे में जानकारी शामिल हैं. %1$s पृष्ठ शीर्षक सबसे ब्राउज़रों अपने विंडो शीर्षक में पता चलता है कि एचटीएमएल %2$s टैग है.", + "PageUrls": "पृष्ठों के यूआरएल", + "PagesReportDocumentation": "इस रिपोर्ट का दौरा किया गया है कि पृष्ठ यूआरएल के बारे में जानकारी शामिल हैं. तालिका %s पदानुक्रम रूप से आयोजित किया जाता है, यूआरएल एक फ़ोल्डर संरचना के रूप में प्रदर्शित कर रहे हैं.", + "PagesSubcategoryHelp3": "नेविगेट करने के लिए बाईं तरफ के प्लस और माइनस आइकॉन का प्रयोग करें.", + "SiteSearchCategories1": "यह रिपोर्ट है कि वे अपनी वेबसाइट पर एक खोज कर दिया जब आगंतुकों द्वारा चयनित श्रेणियाँ सूचीबद्ध करता है.", + "SiteSearchCategories2": "आगंतुकों को एक विशिष्ट श्रेणी में सभी उत्पादों को उनकी खोजों को सीमित कर सकते हैं इतना है कि उदाहरण के लिए, ईकॉमर्स वेबसाइटों आम तौर पर एक \"श्रेणी\" चयनकर्ता है.", + "SiteSearchFollowingPagesDoc": "आगंतुकों को अपनी वेबसाइट पर खोज करते हैं, वे एक विशेष पृष्ठ, सामग्री, उत्पाद या सेवा के लिए देख रहे हैं. इस रिपोर्ट में यह एक आंतरिक खोज के बाद सबसे क्लिक किया गया है कि पृष्ठों की सूची. दूसरे शब्दों में, पृष्ठों की सूची में सबसे पहले से ही अपनी वेबसाइट पर आगंतुकों द्वारा की खोज की.", + "SiteSearchIntro": "आगंतुकों को अपनी वेबसाइट पर बनाने वाली खोजों ट्रैकिंग अपने दर्शकों के लिए क्या देख रहा है के बारे में अधिक जानने के लिए एक बहुत प्रभावी तरीका है, यह संभावित ग्राहकों के लिए खोज, और आम तौर पर आगंतुकों 'में सुधार किया जा सकता है कि नया ईकॉमर्स उत्पादों, नई सामग्री के लिए विचारों को खोजने में मदद कर सकते हैं अपनी वेबसाइट पर अनुभव.", + "SiteSearchKeyword": "खोजशब्द (साइट खोज)", + "SiteSearchKeywordsDocumentation": "यह रिपोर्ट आपकी वेबसाइट के आंतरिक सर्च इंजन पर ढूंढे गए खोजशब्दों को दर्शाती है।", + "SiteSearchKeywordsNoResultDocumentation": "इस रिपोर्ट में किसी भी खोज कीवर्ड को सूचीबद्ध करता है जिनसे खोज परिणाम वापस नहीं आता है शायद खोज इंजन एल्गोरिथ्म में सुधार किया जा सकता है, या हो सकता है आपके आगंतुकों सामग्री के लिए देख रहे हैं वह आपकी वेबसाइट में न हो?", + "SubmenuPageTitles": "पृष्ठों के शीर्षक", + "SubmenuPagesEntry": "प्रवेश पृष्ठ", + "SubmenuPagesExit": "बाहर निकलें पृष्ठों", + "SubmenuSitesearch": "साइट खोज", + "WidgetEntryPageTitles": "प्रवेश प्रष्ट शीर्षक", + "WidgetExitPageTitles": "पृष्ठ शीर्षक से बाहर निकलें", + "WidgetPageTitles": "पृष्ठों के शीर्षक", + "WidgetPageTitlesFollowingSearch": "एक साइट खोज के बाद पृष्ठ शीर्षक", + "WidgetPageUrlsFollowingSearch": "एक साइट खोज के बाद पन्ने", + "WidgetPagesEntry": "प्रवेश पन्ने", + "WidgetPagesExit": "निकास पृष्ठ", + "WidgetSearchCategories": "खोज श्रेणियां", + "WidgetSearchKeywords": "साइट खोजशब्द", + "WidgetSearchNoResultKeywords": "खोजशब्द जिनका कोई परिणाम नहीं" + } +} diff --git a/plugins/Actions/lang/hr.json b/plugins/Actions/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..5eaed7d32bb7d75e5d35beacf246dab5c9d02475 --- /dev/null +++ b/plugins/Actions/lang/hr.json @@ -0,0 +1,92 @@ +{ + "Actions": { + "ActionType": "Vrsta radnje", + "ActionTypeSegmentHelp": "Vrsta radnje, kao što je: %s", + "ActionTypes": "Vrste radnji", + "ActionsInVisit": "Radnje u posjeti", + "AvgGenerationTimeTooltip": "Prosjek na temelju %1$s pogotka %2$s između %3$s i %4$s", + "Behaviour": "Ponašanje", + "ColumnActionURL": "URL radnje", + "ColumnClickedURL": "Kliknuta vanjska poveznica", + "ColumnClickedURLs": "Kliknute vanjske poveznice", + "ColumnClicks": "Klikovi", + "ColumnClicksDocumentation": "Broj pritiskanja ove poveznice.", + "ColumnDownloadURL": "URL za preuzimanje", + "ColumnDownloadURLs": "URL-ovi za preuzimanje", + "ColumnEntryPageTitle": "Naslov ulazne stranice", + "ColumnEntryPageURL": "URL ulazne stranice", + "ColumnEntryPageURLs": "URL-ovi ulazne stranice", + "ColumnExitPageTitle": "Naslov izlazne stranice", + "ColumnExitPageURL": "URL izlazne stranice", + "ColumnExitPageURLs": "URL-ovi izlazne stranice", + "ColumnIdPageview": "ID prikaza stranice", + "ColumnInteractionPosition": "Pozicija interakcije", + "ColumnNoResultKeyword": "Pretraga ključne riječ bez rezultata", + "ColumnPageName": "Ime stranice", + "ColumnPageURL": "URL stranice", + "ColumnPageViewPosition": "Pozicija prikaza stranice", + "ColumnPagesPerSearch": "Stranice s rezultatima pretrage", + "ColumnPagesPerSearchDocumentation": "Posjetitelji će tražiti na tvojoj web-stranici i ponekad koristiti opciju „Dalje” za prikaz dodatnih rezultata pretrage. Ovo je prosječan broj stranica s rezultatima pretrage koji su pogledani za ovu ključnu riječ.", + "ColumnSearchCategory": "Kategorija pretrage", + "ColumnSearchExits": "%% Izlazi nakon pretrage", + "ColumnSearchExitsDocumentation": "Postotak posjetitelja koji su otišli sa vaše stranice nakon traženja ove ključne riječi korištenjem vaše tražilice.", + "ColumnSearchResultsCount": "Broj rezultata pretrage", + "ColumnSearches": "Pretrage", + "ColumnSearchesDocumentation": "Broj posjetitelja koji su tražili ovu ključnu riječ koristeći tražilicu na vašoj stranici.", + "ColumnSiteSearchKeywords": "Jedinstvene ključne riječi", + "ColumnUniqueClicks": "Jedinstveni klikovi", + "ColumnUniqueClicksDocumentation": "Broj posjeta koje su obuhvaćale klik na ovaj link. U slučaju da je link korišten više puta, brojan je samo jednom.", + "ColumnUniqueDownloads": "Jedinstvena preuzimanja", + "ColumnUniqueOutlinks": "Jedinstvene vanjske poveznice", + "DownloadsReportDocumentation": "U ovom izvješću možete vidjeti popis datoteka koje su vaši posjetitelji preuzeli. %s Matomo kao download broji klik na poveznicu za download i ne može znati je li pri tom datoteka zaista u potpunosti preuzeta.", + "DownloadsSubcategoryHelp1": "U ovom izvještaju možeš vidjeti koje su datoteke tvoji posjetitelji preuzeli.", + "EntryPageTitles": "Naslovi ulaznih stranica", + "EntryPageTitlesReportDocumentation": "Ovaj izvještaj sadrži informacije o naslovima korištenih ulaznih stranica unutar određenog razdoblja.", + "EntryPagesReportDocumentation": "Ovaj izveštaj sadrži informacije o ulaznim stranicama koje su korištene tijekom određenog razdoblja. Ulazna stranica je prva stranica koju posjetitelj vidi tijekom svog posjeta. %s URL-ovi ulaza su prikazani kao struktura mapa.", + "EntryPagesSubcategoryHelp1": "Ovaj izveštaj sadrži informacije o ulaznim stranicama koje su korištene tijekom određenog razdoblja. Ulazna stranica je prva stranica koju posjetitelj vidi tijekom svog posjeta.", + "EntryPagesSubcategoryHelp2": "URL-ovi ulaza se prikazuju kao struktura mapa.", + "ExitPageTitles": "Naslovi izlaznih stranica", + "ExitPageTitlesReportDocumentation": "Ovaj izvještaj sadrži informacije o naslovima izlaznih stranica koje su se dogodile u određenom razdoblju.", + "ExitPagesReportDocumentation": "Ovaj izveštaj sadrži informacije o izlaznim stranicama koje su korištene tijekom određenog razdoblja. Izlazna stranica je zadnja stranica koju posjetitelj vidi tijekom svog posjeta. %s URL-ovi izlaza su prikazani kao struktura mapa.", + "ExitPagesSubcategoryHelp1": "Ovaj izveštaj sadrži informacije o izlaznim stranicama koje su korištene tijekom određenog razdoblja. Izlazna stranica je zadnja stranica koju posjetitelj vidi tijekom svog posjeta.", + "MainMetricsReportDocumentation": "Ovaj izvještaj pruža vrlo osnovni pregled radnji koje tvoji posjetitelji poduzimaju na tvojoj web-stranici.", + "OneSearch": "1 pretraga", + "OutlinkDocumentation": "Vanjska poveznica je poveznica koja odvodi tvog posjetitelja s web-stranice (na jednu drugu domenu).", + "OutlinksReportDocumentation": "Ovaj izvještaj prikazuje hijerarhijski popis URL-ova vanjskih poveznica na koje su kliknuli tvoji posjetitelji.", + "OutlinksSubcategoryHelp1": "Ovaj izvještaj prikazuje hijerarhijski popis URL-ova vanjskih poveznica na koje su kliknuli tvoji posjetitelji. Vanjska poveznica je poveznica koja odvodi tvog posjetitelja s web-stranice (na jednu drugu domenu).", + "PageTitlesReportDocumentation": "Ovaj izvještaj sadrži informacije o naslovima posjećenih stranica. %1$s Naslov stranice je HTML oznaka %2$s koju većina preglednika prikazuje u naslovu prozora.", + "PageTitlesSubcategoryHelp1": "Ovaj izvještaj sadrži informacije o naslovima posjećenih stranica.", + "PageUrls": "URL-ovi stranice", + "PageViewsByVisitor": "Ukupni broj posjeta ove stranice od ovog posjetitelja tijekom svih posjeta", + "PagesReportDocumentation": "Ovaj izvještaj sadrži informacije o poveznicama stranica koje su posjećene. %s Tablica je organizirana hijerarhijski, URL-ovi se prikazuju kao struktura mapa.", + "PagesSubcategoryHelp1": "Ovaj izvještaj sadrži informacije o URL-ovima stranica koje su posjećene.", + "PagesSubcategoryHelp3": "Koristite znakove plus i minus sa lijeve strane za navigaciju.", + "PluginDescription": "Izvještaji o pregledima stranica i naslovima stranica. Omogućuje mjerenje pretraživanja tvoje interne tražilice web-stranice. Automatski prati klikove na vanjske poveznice i preuzimanja datoteka.", + "RevisitedPages": "Više puta pogledane stranice", + "SearchesSegmentHelp": "Za biranje svih posjeta koje su koristile internu pretragu web-stranice, koristi: %s", + "SiteSearchCategories": "Kategorije (pretraga web-stranice)", + "SiteSearchCategories1": "Ovaj izvještaj prikazuje kategorije koje su posjetitelji odabrali prilikom pretraživanja tvoje web-stranice.", + "SiteSearchCategories2": "Web-stranice kataloške prodaje najčešće imaju omogućen odabir \"kategorija\" kako bi posjetitelji pretraživali proizvode u željenoj kategoriji.", + "SiteSearchCategory": "Kategorija (pretraga web-stranice)", + "SiteSearchFollowingPagesDoc": "Kada posjetitelji pretražuju tvoju web-stranicu, oni traže određenu stranicu, sadržaj, proizvod ili uslugu. Ovaj izvještaj navodi stranice koje su najviše kliknute nakon internog pretraživanja. Drugim riječima, popis stranica koje su najčešće tražili posjetitelji koji se već nalaze na tvojoj web-stranici.", + "SiteSearchKeyword": "Ključna riječ (pretraga web-stranice)", + "SiteSearchKeywordCount": "Broj ključnih riječi (pretraga web-stranice)", + "SiteSearchKeywordCounts": "Brojevi ključnih riječi (pretraga web-stranice)", + "SiteSearchKeywords": "Ključne riječi (pretraga web-stranice)", + "SubmenuPageTitles": "Naslovi stranica", + "SubmenuPagesEntry": "Ulazne stranice", + "SubmenuPagesExit": "Izlazne stranice", + "SubmenuSitesearch": "Pretraga stranice", + "TimeSpentInReferringAction": "Provedeno vrijeme u radnji upućivanja", + "WidgetEntryPageTitles": "Naslovi ulaznih stranica", + "WidgetExitPageTitles": "Naslovi izlaznih stranica", + "WidgetPageTitles": "Naslovi stranica", + "WidgetPageTitlesFollowingSearch": "Naslovi stranica kod pretraživanja", + "WidgetPageUrlsFollowingSearch": "Stranice kod pretraživanja", + "WidgetPagesEntry": "Ulazne stranice", + "WidgetPagesExit": "Izlazne stranice", + "WidgetSearchCategories": "Pretraži kategoije", + "WidgetSearchKeywords": "Ključne riječi pretraživanja", + "WidgetSearchNoResultKeywords": "Ključne riječi pretraživanja bez rezultata" + } +} diff --git a/plugins/Actions/lang/hu.json b/plugins/Actions/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..5c5c203d63af4646dbe2f259abf0fd4392161587 --- /dev/null +++ b/plugins/Actions/lang/hu.json @@ -0,0 +1,47 @@ +{ + "Actions": { + "ActionType": "Akció típusa", + "ColumnActionURL": "Akció URL", + "ColumnClicks": "Kattintások", + "ColumnClicksDocumentation": "Erre a linkre történt kattintások száma.", + "ColumnDownloadURL": "Letöltött URL", + "ColumnEntryPageTitle": "Oldal név", + "ColumnEntryPageURL": "Oldal Hivatkozás", + "ColumnExitPageTitle": "Kilépő oldal név", + "ColumnExitPageURL": "Kilépő oldal hivatkozás", + "ColumnNoResultKeyword": "Kulcszó keresőtalálat nélkül", + "ColumnPageName": "Weblap címe", + "ColumnPageURL": "Weblap URL-je", + "ColumnPagesPerSearch": "Kereső találati oldalai", + "ColumnPagesPerSearchDocumentation": "A látogatók keresni fognak az ön weboldalán és néha rákattintanak a \"következő\" gombra a további találatok miatt. Ez az átlagosan megtekintett keresési találatok száma erre a kulcsszóra.", + "ColumnSearchCategory": "Kategória keresés", + "ColumnSearches": "Keresések", + "ColumnSiteSearchKeywords": "Egyedi kulcsszavak", + "ColumnUniqueClicks": "Egyedi kattintások", + "ColumnUniqueDownloads": "Egyedi letöltések", + "ColumnUniqueOutlinks": "Egyedi kimenő linkek", + "DownloadsReportDocumentation": "Ez a jelentés azt mutatja, hogy a látogatók melyik fájlokat töltötték le. %s A Matomo a letöltési linkekre való kattintást számolja letöltésnek. Arról, hogy a letöltés valóban befejeződött-e, a Matomo nem tud.", + "EntryPageTitles": "Üres weblap címek", + "EntryPageTitlesReportDocumentation": "Ez a jelentés az adott idő alatt felmerült belépési oldalak címeit és statisztikáit foglalja össze.", + "ExitPageTitles": "Kilépő oldal név", + "ExitPageTitlesReportDocumentation": "Ez a jelentés az adott idő alatt felmerült kilépési oldalak címeit és statisztikáit foglalja össze.", + "OneSearch": "1 keresés", + "PageTitlesReportDocumentation": "Ez a jelentés a megtekintett oldalak címéről tartalmaz információkat. %1$s Az oldal címe a %2$s HTML Tag, amit a legtöbb bönkésző az ablak címsorában mutat.", + "PageUrls": "Oldal hivatkozások", + "PagesReportDocumentation": "Ez a jelentés a megtekintett URL-ekről tartalmaz információkat. %s A tábla hierarchigusan van szervezve, az URL-ek mappaként jelennek meg.", + "PagesSubcategoryHelp3": "Használd a baloldali plusz és mínusz ikonokat a navigációhoz.", + "SiteSearchKeyword": "Kulcsszó (Oldal keresés)", + "SubmenuPageTitles": "Weblapok címei", + "SubmenuPagesEntry": "Belépési lapok", + "SubmenuPagesExit": "Kilépési lapok", + "SubmenuSitesearch": "Oldal keresés", + "WidgetEntryPageTitles": "Belépési lapok címe", + "WidgetExitPageTitles": "Kilépési lapok címe", + "WidgetPageTitles": "Oldalak címe", + "WidgetPagesEntry": "Belépési lapok", + "WidgetPagesExit": "Kilépési lapok", + "WidgetSearchCategories": "Keresés kategóriák", + "WidgetSearchKeywords": "Oldal keresés kulcsszavak", + "WidgetSearchNoResultKeywords": "Kulcsszavak, melyekre nincs találat" + } +} diff --git a/plugins/Actions/lang/hy.json b/plugins/Actions/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..c3956dfad7d320e175bc9c0feeec57667295042e --- /dev/null +++ b/plugins/Actions/lang/hy.json @@ -0,0 +1,25 @@ +{ + "Actions": { + "ColumnClicks": "Կտտոցներ", + "ColumnClicksDocumentation": "Կտտոցների քանակը այս հղման համար", + "ColumnDownloadURL": "Բեռնել URL-ն", + "ColumnEntryPageTitle": "Մուտքային էջի վերնագիր", + "ColumnEntryPageURL": "Մուտքային էջի URL", + "ColumnExitPageTitle": "Ելքային էջի վերնագիր", + "ColumnExitPageURL": "Ելքային էջի URL", + "ColumnPageName": "Էջի վերնագիր", + "ColumnPageURL": "Էջի URL", + "ColumnPagesPerSearch": "Որոնման արդյունքների էջ", + "ColumnSearchCategory": "Որոնման կատեգորիաները", + "ColumnSearches": "Որոնումներ", + "ColumnSiteSearchKeywords": "Ունիկալ որոնվող բառ", + "ColumnUniqueClicks": "Ունիկալ կտտոցներ", + "ColumnUniqueDownloads": "ՈՒնիկալ բեռնումներ", + "EntryPageTitles": "Սկզբնական էջի վերնագիր", + "ExitPageTitles": "Ելքային էջի վերնագիր", + "PageUrls": "Էջի URL հասցեները", + "SubmenuPageTitles": "Էջի որոնում", + "SubmenuPagesExit": "ելքի էջերից", + "WidgetPageTitles": "Էջի Վերնագրեր" + } +} diff --git a/plugins/Actions/lang/id.json b/plugins/Actions/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..9609cd3a5a8a19a6692a8f92134b2194cbab5aff --- /dev/null +++ b/plugins/Actions/lang/id.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Jenis Tindakan", + "ActionTypes": "Jenis Tindakan", + "ActionsInVisit": "Tindakan dalam Kunjungan", + "AvgGenerationTimeTooltip": "Rata-rata berdasarkan %1$s hit (s) %2$s antara %3$s dan %4$s", + "Behaviour": "Perilaku", + "ColumnActionURL": "URL tindakan", + "ColumnClickedURL": "Tautan Keluar yang Diklik", + "ColumnClickedURLs": "Tautan Keluar yang Diklik", + "ColumnClicks": "Klik", + "ColumnClicksDocumentation": "Jumlah tautan ini diklik.", + "ColumnDownloadURL": "URL Unduhan", + "ColumnDownloadURLs": "URL Unduhan", + "ColumnEntryPageTitle": "Judul Halaman Masuk", + "ColumnEntryPageURL": "URL Halaman Masuk", + "ColumnEntryPageURLs": "Alamat web untuk halaman masuk", + "ColumnExitPageTitle": "Judul Halaman Keluar", + "ColumnExitPageURL": "URL Halaman Keluar", + "ColumnExitPageURLs": "Alamat web untuk halaman keluar", + "ColumnIdPageview": "ID Tampilan Halaman", + "ColumnInteractionPosition": "Posisi Interaksi", + "ColumnNoResultKeyword": "Kata Kunci dengan Tanpa Hasil Pencarian", + "ColumnPageName": "Nama Halaman", + "ColumnPageURL": "URL Halaman", + "ColumnPageViewPosition": "Posisi Tampilan Halaman", + "ColumnPagesPerSearch": "Halaman Hasil Pencarian", + "ColumnPagesPerSearchDocumentation": "Pengunjung akan melakukan pencarian dalam situs Anda, dan terkadang mengeklik \"Selanjutnya\" untuk melihat hasil lebih. Ini merupakan jumlah rerata halaman ditampilkan untuk kata kunci ini.", + "ColumnSearchCategory": "Kategori Pencarian", + "ColumnSearchExits": "%% Pencarian Keluar", + "ColumnSearchExitsDocumentation": "Persentase jumlah pengunjung yang meninggalkan situs setelah melakukan pencarian untuk Kata Kunci ini dalam mesin Pencarian Situs Anda.", + "ColumnSearchResultsCount": "Jumlah Hasil Pencarian", + "ColumnSearches": "Pencarian", + "ColumnSearchesDocumentation": "Jumlah kunjungan dengan pencarian kata kunci ini dalam mesin pencari Anda.", + "ColumnSiteSearchKeywords": "Kata Kunci Unik", + "ColumnUniqueClicks": "Klik Unik", + "ColumnUniqueClicksDocumentation": "Jumlah kunjungan yang melibatkan klik pada tautan ini. Jika tautan diklik beberapa kali selama satu kali kunjungan, ini hanya dihitung sekali.", + "ColumnUniqueDownloads": "Unduhan Unik", + "ColumnUniqueOutlinks": "Tautan Keluar yang Unik", + "DownloadsReportDocumentation": "Dalam laporan ini, Anda dapat melihat berkas yang diunduh oleh pengunjung. %s Apa yang Matomo hitung adalah yang mengeklik tautan unduhan. Kondisi unduhan yang sudah lengkap atau belum, Matomo tidak mengetahuinya.", + "DownloadsSubcategoryHelp1": "Dalam laporan ini, Anda dapat melihat berkas mana yang telah diunduh oleh pengunjung Anda.", + "DownloadsSubcategoryHelp2": "Apa yang Matomo hitung sebagai unduhan adalah klik tautan unduhan. Apakah unduhan selesai atau tidak, Matomo tidak mengetahuinya.", + "EntryPageTitles": "Judul halaman masuk", + "EntryPageTitlesReportDocumentation": "Laporan ini mengandung informasi tentang judul halaman masuk yang digunakan selama rentang waktu tertentu.", + "EntryPagesReportDocumentation": "Laporan ini berisi informasi tentang halaman masuk yang digunakan selama periode yang ditentukan. Halaman masuk adalah halaman pertama yang dilihat pengguna selama kunjungan mereka. %s URL entri ditampilkan sebagai struktur folder.", + "EntryPagesSubcategoryHelp1": "Laporan ini berisi informasi tentang halaman entri yang digunakan selama periode tertentu. Halaman entri adalah halaman pertama yang dilihat pengguna selama kunjungan mereka.", + "EntryPagesSubcategoryHelp2": "URL entri ditampilkan sebagai struktur folder.", + "ExitPageTitles": "Judul halaman keluar", + "ExitPageTitlesReportDocumentation": "Laporan ini mengandung informasi tentang judul halman keluar yang terjadi selama waktu tertentu.", + "ExitPagesReportDocumentation": "Laporan ini berisi informasi tentang halaman keluar yang terjadi selama periode yang ditentukan. Halaman keluar adalah halaman terakhir yang dilihat pengguna selama kunjungan mereka. %s URL keluar ditampilkan sebagai struktur folder.", + "ExitPagesSubcategoryHelp1": "Laporan ini berisi informasi tentang halaman keluar yang terjadi selama periode yang ditentukan. Halaman keluar adalah halaman terakhir yang dilihat pengguna selama kunjungan mereka.", + "ExitPagesSubcategoryHelp2": "URL keluar ditampilkan sebagai struktur folder.", + "MainMetricsReportDocumentation": "Laporan ini memberikan gambaran yang sangat mendasar tentang tindakan apa yang dilakukan oleh pengunjung di situs web Anda.", + "OneSearch": "1 pencarian", + "OutlinkDocumentation": "Tautan keluar merupakan tautan yang mengarahkan pengunjung untuk keluar dari situs Anda (ke domain lainnya).", + "OutlinksReportDocumentation": "Laporan ini menunjukkan daftar hierarki URL tautan keluar yang diklik oleh pengunjung Anda.", + "OutlinksSubcategoryHelp1": "Laporan ini menunjukkan daftar hierarki URL tautan keluar yang diklik oleh pengunjung Anda. Tautan keluar adalah tautan yang mengarahkan pengunjung menjauh dari situs web Anda (ke domain lain).", + "PageTitlesReportDocumentation": "Laporan ini berisi informasi tentang judul halaman yang telah dikunjungi. %1$s Judul halaman adalah Tag HTML %2$s yang ditampilkan sebagian besar peramban di judul jendelanya.", + "PageTitlesSubcategoryHelp1": "Laporan ini berisi informasi tentang judul halaman yang telah dikunjungi.", + "PageTitlesSubcategoryHelp2": "Judul halaman adalah HTML &lt;title&gt; Tag yang ditampilkan sebagian besar peramban di judul jendela mereka.", + "PageUrls": "URL Halaman", + "PageViewsByVisitor": "Jumlah dari berapa kali halaman ini dilihat oleh pengunjung dari semua pengunjung", + "PagesReportDocumentation": "Laporan ini berisi informasi tentang URL halaman yang telah dikunjungi. %s Tabel diatur secara hierarkis, URL ditampilkan sebagai struktur folder.", + "PagesSubcategoryHelp1": "Laporan ini berisi informasi tentang URL halaman yang telah dikunjungi.", + "PagesSubcategoryHelp2": "Tabel diatur secara hierarkis, URL ditampilkan sebagai struktur folder.", + "PagesSubcategoryHelp3": "Gunakan ikon plus dan minus di kiri untuk bernavigasi.", + "PluginDescription": "Laporan tentang tampilan halaman dan judul halaman. Memungkinkan Anda untuk mengukur mesin pencari situs web internal Anda. Secara otomatis melacak klik pada tautan eksternal dan unduhan berkas.", + "RevisitedPages": "Halaman yang dilihat lebih dari satu kali", + "SiteSearchCategories": "Kategori (Pencarian situs)", + "SiteSearchCategories1": "Laporan ini mencantumkan Kategori yang dipilih oleh pengunjung saat mereka melakukan Penelusuran di situs web Anda.", + "SiteSearchCategories2": "Misalnya, situs Niaga-el biasanya memiliki pemilih \"Kategori\" yang membatasi pencarian pengunjung terhadap seluruh produk dalam Kategori tertentu.", + "SiteSearchCategory": "Kategori (Pencarian situs)", + "SiteSearchFollowingPagesDoc": "Ketika pengnjung melakukan pencarian di situs Anda, mereka mencari sebagian halaman, isi, produk, atau layanan. Laporan ini mendaftar halaman paling banyak diklik dalam pencarian dalam. Dengan kata lain, daftar halaman paling dicari oleh pengunjung telah tersedia di situs Anda.", + "SiteSearchIntro": "Melacak pencarian yang dilakukan pengunjung di situs web Anda adalah cara yang sangat efektif untuk mempelajari lebih lanjut tentang apa yang dicari audiens Anda, ini dapat membantu dalam menemukan ide untuk membuat konten baru, produk Niaga-el baru yang mungkin dicari oleh calon pelanggan, dan secara umum meningkatkan pengalaman pengunjung di situs web Anda.", + "SiteSearchKeyword": "Kata Kunci (Pencarian Situs)", + "SiteSearchKeywordCount": "Perhitungan kata kunci (Pencarian situs)", + "SiteSearchKeywordCounts": "Perhitungan kata kunci (Pencarian situs)", + "SiteSearchKeywords": "Kata Kunci (Pencarian Situs)", + "SiteSearchKeywordsDocumentation": "Laporan ini mencantumkan Kata Kunci Penelusuran yang ditelusuri oleh pengunjung di Mesin Pencari internal Anda.", + "SiteSearchKeywordsNoResultDocumentation": "Laporan ini mencantumkan Kata Kunci Penelusuran yang tidak menampilkan hasil Penelusuran apa pun: mungkin algoritma mesin telusur dapat ditingkatkan, atau mungkin pengunjung Anda mencari konten yang belum ada di situs web Anda?", + "SiteSearchSubcategoryHelp1": "Bagian Pencarian Situs menunjukkan kata kunci yang digunakan oleh pengunjung saat mencari situs web Anda. Hal ini juga menampilkan halaman mana yang dilihat pengguna setelah melakukan pencarian dan kata kunci pencarian di tempat mana yang tidak menghasilkan hasil sama sekali.", + "SiteSearchSubcategoryHelp2": "Laporan ini dapat memberi Anda ide tentang konten yang tidak ada di situs Anda, wawasan tentang apa yang dicari oleh pengunjung Anda, tetapi tidak dapat ditemukan dengan mudah, dan masih banyak lagi.", + "SiteSearchSubcategoryHelp3": "Pelajari lebih lanjut di panduan Pencarian Situs.", + "SubmenuPageTitles": "Judul halaman", + "SubmenuPagesEntry": "Halaman masuk", + "SubmenuPagesExit": "Halaman keluar", + "SubmenuSitesearch": "Pencarian Situs", + "TimeSpentInReferringAction": "Waktu yang Dihabiskan dalam Merujuk Tindakan", + "WidgetEntryPageTitles": "Judul Halaman Masuk", + "WidgetExitPageTitles": "Judul Halaman Keluar", + "WidgetPageTitles": "Judul Halaman", + "WidgetPageTitlesFollowingSearch": "Judul Halaman Setelah Pencarian Situs", + "WidgetPageUrlsFollowingSearch": "Halaman Setelah Pencarian Situs", + "WidgetPagesEntry": "Halaman Masuk", + "WidgetPagesExit": "Halaman Keluar", + "WidgetSearchCategories": "Kategori Pencarian", + "WidgetSearchKeywords": "Kata Kunci Pencarian Situs", + "WidgetSearchNoResultKeywords": "Kata Kunci Pencarian dengan Tanpa Hasil" + } +} diff --git a/plugins/Actions/lang/is.json b/plugins/Actions/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..9b4a71d57f2a6ea3de6fdfc58bc3dff2f6581d0c --- /dev/null +++ b/plugins/Actions/lang/is.json @@ -0,0 +1,13 @@ +{ + "Actions": { + "ColumnClicks": "Músasmellir", + "ColumnDownloadURL": "Niðurhals slóð", + "ColumnPageName": "Síðu nafn", + "ColumnPageURL": "Síðu slóð", + "ColumnUniqueClicks": "Einstakir músasmellir", + "ColumnUniqueDownloads": "Einstakt niðurhal", + "SubmenuPageTitles": "Síðu titlar", + "SubmenuPagesEntry": "Inngangssíður", + "SubmenuPagesExit": "Útgangssíður" + } +} diff --git a/plugins/Actions/lang/it.json b/plugins/Actions/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..c9a90b36ac3086f70895556cb1fb5ad7fcc63b9e --- /dev/null +++ b/plugins/Actions/lang/it.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Tipo di Azione", + "ActionTypeSegmentHelp": "Un tipo di azione, come: %s", + "ActionTypes": "Tipi di Azione", + "ActionsInVisit": "Azioni su Visita", + "AvgGenerationTimeTooltip": "Media basata su %1$s hits %2$s tra %3$s e %4$s", + "Behaviour": "Comportamento", + "ColumnActionURL": "URL dell'azione", + "ColumnClickedURL": "Link in Uscita Cliccato", + "ColumnClickedURLs": "Link in Uscita Cliccati", + "ColumnClicks": "Click", + "ColumnClicksDocumentation": "Numero di volte che questo link è stato cliccato.", + "ColumnDownloadURL": "URL Download", + "ColumnDownloadURLs": "URL Download", + "ColumnEntryPageTitle": "Titolo pagina di entrata", + "ColumnEntryPageURL": "URL pagina di entrata", + "ColumnEntryPageURLs": "URL Pagine di entrata", + "ColumnExitPageTitle": "Titolo pagina di uscita", + "ColumnExitPageURL": "URL pagina di uscita", + "ColumnExitPageURLs": "URL Pagine di Uscita", + "ColumnIdPageview": "ID Pagina vista", + "ColumnInteractionPosition": "Posizione Interazione", + "ColumnNoResultKeyword": "Parole chiave senza risultati di ricerca", + "ColumnPageName": "Nome pagina", + "ColumnPageURL": "URL pagina", + "ColumnPageViewPosition": "Posizione in Visualizzazione di pagina", + "ColumnPagesPerSearch": "Pagine di risultati", + "ColumnPagesPerSearchDocumentation": "I visitatori cercheranno sul sito e delle volte cliccheranno su \"Successivi\" per vedere più risultati. Questo è il numero medio di risultati di ricerca visualizzati per questa parola chiave.", + "ColumnSearchCategory": "Categoria di Ricerca", + "ColumnSearchExits": "%% Esiti Ricerca", + "ColumnSearchExitsDocumentation": "La percentuale di visite che hanno lasciato il sito dopo aver ricercato questa parola chiave sul motore di ricerca del vostro sito.", + "ColumnSearchResultsCount": "Numero risultati di ricerca", + "ColumnSearches": "Ricerche", + "ColumnSearchesDocumentation": "Il numero di visite che hanno ricercato questa parola chiave sul motore di ricerca del vostro sito.", + "ColumnSiteSearchKeywords": "Parole chiave uniche", + "ColumnUniqueClicks": "Click Unici", + "ColumnUniqueClicksDocumentation": "Il numero di visite che hanno coinvolto un click su questo link. Se un link è stato cliccato più volte nel corso di una visita, viene conteggiato una sola volta.", + "ColumnUniqueDownloads": "Download Unici", + "ColumnUniqueOutlinks": "Link esterni unici", + "DownloadsReportDocumentation": "In questo report è possibile vedere quali file sono stati scaricati dai tuoi visitatori. %s Ciò che Matomo conta come download è il click su un link per il download. Se il download è stato completato o meno, non è noto a Matomo.", + "DownloadsSubcategoryHelp1": "In questo report puoi vedere quali file hanno scaricato i tuoi visitatori.", + "DownloadsSubcategoryHelp2": "Ciò che Matomo conta come download è il click su un collegamento per il download. Matomo non sa se il download è stato completato o meno.", + "EntryPageTitles": "Titoli delle pagine di entrata", + "EntryPageTitlesReportDocumentation": "Questo report contiene informazioni sui titoli delle pagine di entrata che sono state utilizzate durante il periodo specificato.", + "EntryPagesReportDocumentation": "Questo report contiene le informazioni sulle pagine di entrata utilizzate nel periodo di tempo definito. Una pagina di entrata è la prima pagina che un utente vede durante la sua visita. %s Le URL di ingresso vengono visualizzate come una struttura a cartelle.", + "EntryPagesSubcategoryHelp1": "Questo report contiene informazioni sulle pagine di entrata utilizzate durante il periodo specificato. Una pagina di ingresso è la prima pagina che un utente visualizza durante la sua visita.", + "EntryPagesSubcategoryHelp2": "Le pagine di entrata sono visualizzate come una struttura di cartelle.", + "ExitPageTitles": "Titoli delle pagine di uscita", + "ExitPageTitlesReportDocumentation": "Questo report contiene informazioni sui titoli delle pagine di uscita durante il periodo specificato.", + "ExitPagesReportDocumentation": "Questo report contiene le informazioni sulle pagine di uscita utilizzate nel periodo di tempo definito. Una pagina di uscita è l'ultima pagina che un utente vede durante la sua visita. %s Le URL di uscita vengono visualizzate come una struttura a cartelle.", + "ExitPagesSubcategoryHelp1": "Questo report contiene informazioni sulle pagine di uscita utilizzate durante il periodo specificato. Una pagina di uscita è l'ultima pagina che un utente visualizza durante la sua visita.", + "ExitPagesSubcategoryHelp2": "Le pagine di uscita sono visualizzate come una struttura di cartelle.", + "MainMetricsReportDocumentation": "Questo report fornisce una panoramica molto semplice delle azioni intraprese dai visitatori sul tuo sito web.", + "OneSearch": "1 ricerca", + "OutlinkDocumentation": "Un link esterno è un link che conduce il visitatore lontano dal tuo sito web (in un altro dominio)", + "OutlinksReportDocumentation": "Questo report mostra un elenco gerarchico di link esterni che sono stati cliccati dai visitatori.", + "OutlinksSubcategoryHelp1": "Questo report mostra un elenco gerarchico di URL di link di uscita su cui hanno cliccato i tuoi visitatori. Un link di uscita è un link che porta il visitatore lontano dal tuo sito web (verso un altro dominio).", + "PageTitlesReportDocumentation": "Questo report contiene informazioni sui titoli delle pagine che sono state visitate. %1$s Il titolo della pagina è il tag HTML %2$s che la maggior parte dei browser mostrano nel titolo della loro finestra.", + "PageTitlesSubcategoryHelp1": "Questo report contiene informazioni sui titoli delle pagine che sono state visitate.", + "PageTitlesSubcategoryHelp2": "Il titolo della pagina è il Tag HTML & lt; title& gt; Tag che la maggior parte dei browser mostra nel titolo della finestra.", + "PageUrls": "URL delle pagine", + "PageViewsByVisitor": "Numero di volte che questa pagina è stata vista da questo visitatore in tutte le sue visite.", + "PagesReportDocumentation": "Questo report contiene informazioni riguardo gli URL delle pagine che sono state viste. %s La tabella è organizzata gerarchicamente, gli URL vengono visualizzati come una struttura di cartelle.", + "PagesSubcategoryHelp1": "Questo report contiene informazioni sugli URL delle pagine che sono state visitate.", + "PagesSubcategoryHelp2": "La tabella è organizzata in maniera gerarchica, gli URL vengono visualizzati come una struttura di cartelle.", + "PagesSubcategoryHelp3": "Usa le icone + e - per navigare.", + "PluginDescription": "Rapporto relativo a viste e i titoli pagina. Ti consente di misurare il motore di ricerca interno al tuo sito. Traccia in automatico i click sui link esterni e i download dei files.", + "RevisitedPages": "Pagine viste più di una volta.", + "SearchesSegmentHelp": "Per selezionare tutte le visite che hanno utilizzato la Ricerca interna del sito, utilizzare: %s", + "SiteSearchCategories": "Categorie (Ricerca nel Sito)", + "SiteSearchCategories1": "Questo report elenca le Categorie che i visitatori hanno selezionato nel fare una ricerca sul vostro sito.", + "SiteSearchCategories2": "Per esempio, i siti di Ecommerce hanno tipicamente un selettore di \"Categoria\", così che i visitatori possano restringere le ricerche ai prodotti in una Categoria specifica.", + "SiteSearchCategory": "Categoria (Ricerca nel Sito)", + "SiteSearchFollowingPagesDoc": "Quando i visitatori fanno ricerche sul vostro sito, stanno cercando una determinata pagina, contenuto, prodotto e servizio. Questo report elenca le pagine più cliccate dopo una ricerca interna. In altre parole, è l'elenco delle pagine più ricercate dai visitatori sul vostro sito.", + "SiteSearchIntro": "Tracciare le ricerche che i visitatori fanno sul vostro sito è un modo molto efficace per imparare di più su quello che cercano coloro che vi seguono, può aiutare a trovare idee per nuovi contenuti, nuovi prodotti Ecommerce ricercati da potenziali clienti, e generalmente migliora l'esperienza dei visitarori sul vostro sito.", + "SiteSearchKeyword": "Parola chiave (Ricerca sito)", + "SiteSearchKeywordCount": "Conteggio Keyword (Ricerca nel sito)", + "SiteSearchKeywordCounts": "Conteggi Keyword (Ricerca nel sito)", + "SiteSearchKeywords": "Keywords (Ricerca nel Sito)", + "SiteSearchKeywordsDocumentation": "Questo report elenca le Parole di Ricerca che i visitatori hanno digitato nel vostro Motore di Ricerca interno.", + "SiteSearchKeywordsNoResultDocumentation": "Questo report elenca la Parole di Ricerca che non hanno restituito alcun risultato: forse può essere migliorato l'algoritmo del motore di ricerca o forse i vostri visitatori stanno cercando un contenuto che non è (ancora) sul vostro sito?", + "SiteSearchSubcategoryHelp1": "La sezione Ricerca nel sito mostra le parole chiave utilizzate dai visitatori durante le ricerche nel tuo sito web. Mostra anche quali pagine visualizzano gli utenti dopo aver eseguito una ricerca e quali parole chiave di ricerca sul sito non restituiscono alcun risultato.", + "SiteSearchSubcategoryHelp2": "Questi report possono darti delle idee sui contenuti mancanti nel tuo sito, informazioni su ciò che i tuoi visitatori cercano ma non riescono a trovare facilmente e altro ancora.", + "SiteSearchSubcategoryHelp3": "Ulteriori informazioni nella guida Ricerca sul sito.", + "SubmenuPageTitles": "Titoli delle pagine", + "SubmenuPagesEntry": "Pagine di entrata", + "SubmenuPagesExit": "Pagine di uscita", + "SubmenuSitesearch": "Ricerca sul Sito", + "TimeSpentInReferringAction": "Tempo trascorso nell'azione di riferimento", + "WidgetEntryPageTitles": "Titoli delle pagine di entrata", + "WidgetExitPageTitles": "Titoli pagine di uscita", + "WidgetPageTitles": "Titoli delle pagine", + "WidgetPageTitlesFollowingSearch": "Titoli Pagina risultanti da una Ricerca sul Sito", + "WidgetPageUrlsFollowingSearch": "Pagine risultanti da una Ricerca sul Sito", + "WidgetPagesEntry": "Pagine di entrata", + "WidgetPagesExit": "Pagine di uscita", + "WidgetSearchCategories": "Categorie di Ricerca", + "WidgetSearchKeywords": "Parole Chiave di Ricerca sul Sito", + "WidgetSearchNoResultKeywords": "Parole Chiave di Ricerca senza risultati" + } +} diff --git a/plugins/Actions/lang/ja.json b/plugins/Actions/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..b0b1bf490ff65da4a896444cfaa809a156aef9e9 --- /dev/null +++ b/plugins/Actions/lang/ja.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "アクションタイプ", + "ActionTypes": "アクションタイプ", + "ActionsInVisit": "ビジット単位のアクション数", + "AvgGenerationTimeTooltip": "%3$s から %4$s の間の %2$s のヒット ( 複数 ) %1$s に基づく平均", + "Behaviour": "行動", + "ColumnActionURL": "アクション URL", + "ColumnClickedURL": "クリックされたアウトリンク", + "ColumnClickedURLs": "クリックされたアウトリンク", + "ColumnClicks": "クリック", + "ColumnClicksDocumentation": "このリンクがクリックされた回数です。", + "ColumnDownloadURL": "ダウンロード URL", + "ColumnDownloadURLs": "ダウンロード URL", + "ColumnEntryPageTitle": "入口ページタイトル", + "ColumnEntryPageURL": "入口ページ URL", + "ColumnEntryPageURLs": "エントリーページ URL", + "ColumnExitPageTitle": "出口ページタイトル", + "ColumnExitPageURL": "出口ページ URL", + "ColumnExitPageURLs": "出口ページ URL", + "ColumnIdPageview": "ページビューID", + "ColumnInteractionPosition": "インタラクションポジション", + "ColumnNoResultKeyword": "検索結果がみつかりませんでした", + "ColumnPageName": "ページ名", + "ColumnPageURL": "ページ URL", + "ColumnPageViewPosition": "ページビューの位置", + "ColumnPagesPerSearch": "検索結果ページ", + "ColumnPagesPerSearchDocumentation": "ビジターはあなたのウェブサイトを検索し、時々もっと結果を見るために \"次へ\"をクリックします。 これは、このキーワードに対して表示された検索結果ページの平均数です。", + "ColumnSearchCategory": "検索カテゴリー", + "ColumnSearchExits": "%% 検索終了", + "ColumnSearchExitsDocumentation": "サイトの検索機能を使って、このキーワードを検索した後、ウェブサイトを離れたビジターの割合。", + "ColumnSearchResultsCount": "検索結果数", + "ColumnSearches": "検索", + "ColumnSearchesDocumentation": "Webサイトの検索機能でこのキーワードを検索したビジット数。", + "ColumnSiteSearchKeywords": "ユニークキーワード", + "ColumnUniqueClicks": "ユニーククリック", + "ColumnUniqueClicksDocumentation": "このリンクをクリックしたビジターの数です。一回のビジットで複数回リンクがクリックされても、一回としてカウントされます。", + "ColumnUniqueDownloads": "ユニークダウンロード数", + "ColumnUniqueOutlinks": "ユニーク外部リンク数", + "DownloadsReportDocumentation": "ビジターがどのファイルをダウンロードしたかについてのレポートです。%s Matomoはダウンロードリンクのクリック数をカウントします。ダウンロードが完了したかどうかは判別できません。", + "DownloadsSubcategoryHelp1": "このレポートでは、ビジターがダウンロードしたファイルを確認できます。", + "DownloadsSubcategoryHelp2": "Matomoがダウンロードとしてカウントするのは、ダウンロードリンクをクリックすることです。 ダウンロードが完了したかどうかは、Matomoにはわかりません。", + "EntryPageTitles": "入口ページタイトル", + "EntryPageTitlesReportDocumentation": "これは期間中の入口ページタイトルについてのレポートです。", + "EntryPagesReportDocumentation": "このレポートには、指定した期間中に使用されたエントリーのページに関する情報が含まれています。 エントリーページは、ユーザーが訪問中に見る最初のページです。%s エントリ URL は、フォルダ構造として表示されます。", + "EntryPagesSubcategoryHelp1": "このレポートには、指定された期間に使用されたエントリページに関する情報が含まれています。 エントリページは、ユーザーが訪問中に表示する最初のページです。", + "EntryPagesSubcategoryHelp2": "エントリURLはフォルダ構造として表示されます。", + "ExitPageTitles": "出口ページタイトル", + "ExitPageTitlesReportDocumentation": "これは期間中の出口ページタイトルについてのレポートです。", + "ExitPagesReportDocumentation": "このレポートには、指定した期間中に発生した出口ページに関する情報が含まれています。 出口ページは、ユーザーが訪問中に最後に表示したページです。 %s 出口 URL はフォルダ構造として表示されます。", + "ExitPagesSubcategoryHelp1": "このレポートには、指定された期間中に発生した終了ページに関する情報が含まれています。 終了ページは、ユーザーが訪問中に表示する最後のページです。", + "ExitPagesSubcategoryHelp2": "出口URLはフォルダ構造として表示されます。", + "MainMetricsReportDocumentation": "このレポートは、ビジターがWebサイトで実行するアクションの非常に基本的な概要を提供します。", + "OneSearch": "1 サーチ", + "OutlinkDocumentation": "外部リンクとはサイト内から外部のサイトへ(他のドメインへ)のリンクです。", + "OutlinksReportDocumentation": "ビジターがクリックした外部リンクの URL を階層構造のリストで表示したレポートです。", + "OutlinksSubcategoryHelp1": "このレポートには、ビジターがクリックしたアウトリンクURLの階層リストが表示されます。 アウトリンクは、ビジターをWebサイトから(別のドメインに)誘導するリンクです。", + "PageTitlesReportDocumentation": "これは訪問されたページタイトルについてのレポートです。%1$s ページタイトルは多くのブラウザでウインドウのタイトルに表示される HTML の %2$s タグです。", + "PageTitlesSubcategoryHelp1": "このレポートには、アクセスしたページのタイトルに関する情報が含まれています。", + "PageTitlesSubcategoryHelp2": "ページタイトルは、ほとんどのブラウザがウィンドウタイトルに表示するHTMLの&lt;title&gt;タグです。", + "PageUrls": "ページ URL", + "PageViewsByVisitor": "すべてのビジットでこのビジターがこのページを閲覧した回数", + "PagesReportDocumentation": "これは訪問されたページ URL についてのレポートです。%s 表は階層構造になっており、URL はフォルダーの階層で表示されています。", + "PagesSubcategoryHelp1": "このレポートには、アクセスされたページのURLに関する情報が含まれています。", + "PagesSubcategoryHelp2": "テーブルは階層的に編成されており、URLはフォルダ構造として表示されます。", + "PagesSubcategoryHelp3": "左側のプラス記号とマイナス記号のアイコンをクリックして操作します。", + "PluginDescription": "ページビューやページタイトルをレポートします。内部のウェブサイトの検索エンジンを測定することができます。外部リンクのクリックやファイルのダウンロードを自動的に追跡します。", + "RevisitedPages": "複数回閲覧されたページ", + "SiteSearchCategories": "カテゴリー ( サイト検索 )", + "SiteSearchCategories1": "このレポートは、ビジターがサイトで検索した時に選択したカテゴリの一覧です。", + "SiteSearchCategories2": "例えば、e コマースサイトは一般的に \" カテゴリー \" のセレクトを持っていて、ビジターは製品の検索結果を特定のカテゴリーに絞り込みできます。", + "SiteSearchCategory": "カテゴリー ( サイト検索 )", + "SiteSearchFollowingPagesDoc": "ビジターは、特定のページ、コンテンツ、製品、またはサービスを見つけようとして、サイト内で検索を行います。このレポートは、サイト内で検索してから最も多くクリックされたページの一覧です。言い換えれば、サイト内でビジターが最も検索したページの一覧ということです。", + "SiteSearchIntro": "ビジターの検索を追跡することは、あなたのウェブサイトを改善するのにとても効果的です。顧客が探しているものについての詳細を知ることができ、新しいコンテンツのアイデアを見つけるのに役立ち、潜在的な顧客が求める新たな e コマースの製品を提供することができるようになります。そして、ビジターの Web エクスペリエンスを大幅に向上させます。", + "SiteSearchKeyword": "キーワード(サイト検索)", + "SiteSearchKeywordCount": "キーワード数(サイト検索)", + "SiteSearchKeywordCounts": "キーワード数(サイト検索)", + "SiteSearchKeywords": "キーワード ( サイト検索 )", + "SiteSearchKeywordsDocumentation": "このレポートは、ビジターがサイト内検索機能で検索したキーワードの一覧を表示します。", + "SiteSearchKeywordsNoResultDocumentation": "このレポートは、検索結果が 0 件であったキーワードの一覧を表示します。恐らく検索機能のアルゴリズムの改善が必要か、もしくはビジターはあなたのウェブサイトに ( まだ ) ないコンテンツを探しているのでしょう?", + "SiteSearchSubcategoryHelp1": "[サイト検索]セクションには、ビジターがWebサイトを検索するときに使用するキーワードが表示されます。 また、ユーザーが検索を実行した後に表示するページと、結果をまったく返さないオンサイト検索キーワードも表示されます。", + "SiteSearchSubcategoryHelp2": "これらのレポートは、サイトに不足しているコンテンツに関するアイデア、ビジターが探しているものの簡単には見つけられないものに関する洞察などを提供します。", + "SiteSearchSubcategoryHelp3": "詳細については、サイト検索ガイドをご覧ください。", + "SubmenuPageTitles": "ページタイトル", + "SubmenuPagesEntry": "入口ページ", + "SubmenuPagesExit": "出口ページ", + "SubmenuSitesearch": "サイト検索", + "TimeSpentInReferringAction": "参照アクションに費やされた時間", + "WidgetEntryPageTitles": "入口ページタイトル", + "WidgetExitPageTitles": "出口ページタイトル", + "WidgetPageTitles": "ページタイトル", + "WidgetPageTitlesFollowingSearch": "サイト検索後に見られたページタイトル", + "WidgetPageUrlsFollowingSearch": "サイト検索後に見られたページ", + "WidgetPagesEntry": "入口ページ", + "WidgetPagesExit": "出口ページ", + "WidgetSearchCategories": "検索カテゴリー", + "WidgetSearchKeywords": "サイト検索キーワード", + "WidgetSearchNoResultKeywords": "検索結果無しの検索キーワード" + } +} diff --git a/plugins/Actions/lang/ka.json b/plugins/Actions/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..138f23b33c717c943caad6ad9a8e495ca41398a9 --- /dev/null +++ b/plugins/Actions/lang/ka.json @@ -0,0 +1,13 @@ +{ + "Actions": { + "ColumnClicks": "დაწკაპუნებები", + "ColumnDownloadURL": "URL–ის ჩამოტვირთვა", + "ColumnPageName": "გვერდის სახელი", + "ColumnPageURL": "გვერდის URL", + "ColumnUniqueClicks": "უნიკალური დაწკაპუნებები", + "ColumnUniqueDownloads": "უნიკალური ჩამოტვირთვები", + "SubmenuPageTitles": "გვერდის სათაურები", + "SubmenuPagesEntry": "შესვლის გვერდები", + "SubmenuPagesExit": "გასავლის გვერდები" + } +} diff --git a/plugins/Actions/lang/ko.json b/plugins/Actions/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..b976787f88ab2320f7f8c127ecf79cc8b3583177 --- /dev/null +++ b/plugins/Actions/lang/ko.json @@ -0,0 +1,69 @@ +{ + "Actions": { + "ActionType": "활동 종류", + "ActionTypes": "활동 종류들", + "ColumnActionURL": "활동 URL", + "ColumnClicks": "조회수", + "ColumnClicksDocumentation": "이 링크를 클릭 한 횟수입니다.", + "ColumnDownloadURL": "다운로드 URL", + "ColumnDownloadURLs": "다운로드 URL들", + "ColumnEntryPageTitle": "진입 페이지 제목", + "ColumnEntryPageURL": "진입 페이지 URL", + "ColumnEntryPageURLs": "진입 페이지 URL들", + "ColumnExitPageTitle": "이탈 페이지 제목", + "ColumnExitPageURL": "이탈 페이지 URL", + "ColumnExitPageURLs": "이탈 페이지 URL들", + "ColumnIdPageview": "페이지뷰 아이디", + "ColumnInteractionPosition": "상호작용 위치", + "ColumnNoResultKeyword": "검색 결과 없음", + "ColumnPageName": "페이지 이름", + "ColumnPageURL": "페이지 URL", + "ColumnPagesPerSearch": "검색 결과 페이지", + "ColumnPagesPerSearchDocumentation": "방문객은 당신의 웹사이트에서 검색하고 때로는 더 많은 검색 결과를 보기위해 \"다음\"을 클릭합니다. 이 검색어로 인하여 발생한 페이지뷰 수의 평균입니다.", + "ColumnSearchCategory": "검색 카테고리", + "ColumnSearchExits": "%% 검색 종료", + "ColumnSearchExitsDocumentation": "이 검색어로 당신의 사이트에서 검색시도 후 웹사이트를 떠난 방문의 비율입니다.", + "ColumnSearchResultsCount": "검색 결과 수", + "ColumnSearches": "검색", + "ColumnSearchesDocumentation": "웹사이트의 검색 엔진에서 검색어별 방문 횟수입니다.", + "ColumnSiteSearchKeywords": "고유 검색어", + "ColumnUniqueClicks": "고유 조회수", + "ColumnUniqueClicksDocumentation": "이 링크를 클릭 한 방문자의 수입니다. 한번의 방문으로 여러 번 링크를 클릭해도 한 번으로 계산됩니다.", + "ColumnUniqueDownloads": "고유 다운로드 수", + "ColumnUniqueOutlinks": "고유한 외부 링크 수", + "DownloadsReportDocumentation": "이 보고서는 방문자가 어떤 파일을 다운로드했는지에 대한 정보입니다. %s Matomo는 다운로드 링크의 클릭 수를 계산합니다. 하지만 다운로드가 완료되었는지 여부는 확인할 수 없습니다.", + "EntryPageTitles": "진입 페이지 제목", + "EntryPageTitlesReportDocumentation": "이 보고서는 기간별 진입 페이지 제목에 대한 정보입니다.", + "ExitPageTitles": "이탈 페이지 제목", + "ExitPageTitlesReportDocumentation": "이 보고서는 기간의 이탈 페이지 제목에 대한 정보입니다.", + "OneSearch": "검색 1", + "OutlinkDocumentation": "외부 링크는 사이트 내에서 외부 사이트(다른 도메인)로의 링크입니다.", + "OutlinksReportDocumentation": "이 보고서는 방문자가 클릭 한 외부 링크의 URL을 계층 구조 목록으로 표시합니다.", + "PageTitlesReportDocumentation": "이 보고서는 방문한 페이지 제목에 대한 정보입니다. %1$s 페이지 제목은 대부분의 브라우저 윈도우의 제목에 표시되는 HTML %2$s 태그입니다.", + "PageUrls": "페이지 URL", + "PagesReportDocumentation": "이 보고서는 방문한 페이지 URL에 대한 정보입니다. %s 테이블은 계층 구조로 되어 있으며, URL은 폴더 구조로 표시됩니다.", + "PagesSubcategoryHelp3": "왼쪽의 더하기와 빼기 아이콘을 클릭하여 작업합니다.", + "PluginDescription": "페이지 뷰와 제목에 대한 보고서. 내부 웹사이트 검색 엔진을 측정할 수 있습니다. 외부 링크 클릭 및 파일 다운로드에 대해서도 자동으로 추적합니다.", + "SiteSearchCategories1": "이 보고서는 웹사이트에서 검색했을 때 방문자가 선택한 카테고리를 나열합니다.", + "SiteSearchCategories2": "예를 들면, 일반적인 전자상거래 사이트는 '카테고리'를 제공하는데 방문자는 특정 카테고리의 모든 제품을 검색하는 것으로 범위를 축소할 수 있습니다.", + "SiteSearchFollowingPagesDoc": "방문자는 특정 페이지, 콘텐츠, 제품 또는 서비스를 찾으려고 웹사이트에서 검색을 시도합니다. 이 보고서는 사이트 내부에서 검색시도 후 가장 많이 클릭한 페이지 목록입니다. 즉, 이 페이지 목록은 대부분의 방문자가 사이트내 검색에 의해 방문한 것입니다.", + "SiteSearchIntro": "방문자의 검색을 추적하는 것은 당신의 웹사이트를 좋게 만드는 매우 효과적인 방법입니다. 잠재 고객이 무엇을 찾고 있는지에 대해 자세히 알 수 있고, 이것으로 새로운 콘텐츠에 대한 아이디어를 찾는 데 도움을 받게 되며, 잠재 고객이 찾는 새로운 전자상거래 제품을 제공할 수 있게 됩니다. 그리고 방문자의 웹사이트 사용 경험을 대폭 향상시킵니다.", + "SiteSearchKeyword": "키워드 (사이트 검색)", + "SiteSearchKeywordsDocumentation": "이 보고서에서는 방문자가 내부 검색 엔진에서 검색한 검색어를 나열합니다.", + "SiteSearchKeywordsNoResultDocumentation": "이 보고서는 검색 결과를 반환하지 않은 검색어를 나열합니다: 어쩌면 검색 엔진 알고리즘을 향상 시키거나 방문자가 당신의 웹사이트에 (아직)없는 콘텐츠를 찾고 있는 것이겠죠?", + "SubmenuPageTitles": "페이지 제목", + "SubmenuPagesEntry": "진입 페이지", + "SubmenuPagesExit": "이탈 페이지", + "SubmenuSitesearch": "사이트 검색", + "WidgetEntryPageTitles": "진입 페이지 제목", + "WidgetExitPageTitles": "이탈 페이지 제목", + "WidgetPageTitles": "페이지 제목", + "WidgetPageTitlesFollowingSearch": "페이지 제목별 사이트 검색", + "WidgetPageUrlsFollowingSearch": "페이지별 사이트 검색", + "WidgetPagesEntry": "진입 페이지", + "WidgetPagesExit": "이탈 페이지", + "WidgetSearchCategories": "검색 카테고리", + "WidgetSearchKeywords": "사이트 검색어", + "WidgetSearchNoResultKeywords": "검색 결과 없음" + } +} diff --git a/plugins/Actions/lang/ku.json b/plugins/Actions/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..946ae53dcdaa9865d340e5a1e7c61eaf1996a6b1 --- /dev/null +++ b/plugins/Actions/lang/ku.json @@ -0,0 +1,39 @@ +{ + "Actions": { + "ActionsInVisit": "چالاکییەکانی سەردان", + "AvgGenerationTimeTooltip": "مامناوەند بەپێی %1$s کرتە %2$s لەنێوان %3$s و %4$s", + "ColumnActionURL": "ناونیشانی چالاکی", + "ColumnClickedURL": "بەستەری دەرەکی کرتەکراو", + "ColumnClickedURLs": "بەستەرە دەرەکییە کرتەکراوەکان", + "ColumnClicks": "کرتەکان", + "ColumnClicksDocumentation": "ژمارەی ئەو جارانەی ئەم بەستەرە کرتەی لێکراوە", + "ColumnDownloadURL": "ناونیشانی داگرتن", + "ColumnDownloadURLs": "ناونیشانەکانی داگرتن", + "ColumnEntryPageTitle": "سەردێری لاپەڕەی تێهاتن", + "ColumnEntryPageURL": "ناونیشانی لاپەڕەی تێهاتن", + "ColumnEntryPageURLs": "ناونیشانی لاپەڕەکانی تێهاتن", + "ColumnExitPageTitle": "سەردێری لاپەڕەی دەرچوون", + "ColumnExitPageURL": "ناونیشانی لاپەڕەی دەرچوون", + "ColumnExitPageURLs": "ناونیشانی لاپەڕەکانی دەرچوون", + "ColumnIdPageview": "پێناسەی بینینی پەڕە", + "ColumnInteractionPosition": "شوێنی کارلێک", + "ColumnNoResultKeyword": "کلیلوشە بەبێ ئەنجامی گەڕان", + "ColumnPageName": "ناوی لاپەڕە", + "ColumnPageViewPosition": "شوێنی بینینی پەڕە", + "ColumnPagesPerSearch": "لاپەڕەکانی ئاکامەکانی گەڕان", + "ColumnPagesPerSearchDocumentation": "سەردانکەران لە وێبسایتەکەتدا دەگەڕێن، و هەندێک جار کرتە لەسەر \"دواتر\" دەکەن بۆ بینینی ئەنجامی زیاتر. ئەمە تێکڕای ژمارەی لاپەڕەکانی ئەنجامەکانی گەڕانە کە بۆ ئەم کلیلوشە نیشان دراوە.", + "ColumnSearchCategory": "پۆلی گەڕان", + "ColumnSearchExits": "%% گەڕان دەرچووە", + "ColumnSearchExitsDocumentation": "نیسبەتی ئەو سەردانانەی کە دوای گەڕان بەدوای ئەم کلیلوشەیەدا لەسەر مۆتۆری گەڕانی وێبسایتەکەت، وێبسایتەکەیان بەجێ هێشتووە.", + "ColumnSearchResultsCount": "ژمارەی ئاکامەکانی گەڕان", + "ColumnSearches": "گەڕانەکان", + "ColumnSearchesDocumentation": "ژمارەی ئەو سەردانانە کە بۆ ئەم کلیلوشەیە گەڕاون لە مۆتۆری گەڕانی وێبسایتەکەتدا.", + "ColumnSiteSearchKeywords": "کلیلوشە تاقانەکان", + "ColumnUniqueClicks": "کرتە تاقانەکان", + "ColumnUniqueClicksDocumentation": "ژمارەی ئەو سەردانانەی کە کرتەیەکی لەسەر ئەم لینکەدا هەیە. ئەگەر بەستەرێ چەند جارێک کرتە کرا بێت لە کاتی یەک سەرداندا، تەنها جارێک دەژمێردرێت.", + "ColumnUniqueDownloads": "داگرتنە تاقانەکان", + "ColumnUniqueOutlinks": "بەستەرە دەرەکەییە تاقانەکان", + "DownloadsReportDocumentation": "لەم ڕاپۆرتەدا، دەتوانیت ببینیت سەردانکەران کام فایلانەیان داگرتووە. %s ئەوەی مەتومۆ وەک داگرتن دەیژمێرێت، کرتەکردنە لەسەر لینکی داگرتن. ئەگەرچی داگرتنەکە تەواونەبووە یان لەلایەن مەتۆمۆوە نەزانرابێ.", + "PagesSubcategoryHelp3": "بۆ گەڕان لە لای چەپەوە ئایکۆنی کۆ و کەمکردن بەکار بێنە." + } +} diff --git a/plugins/Actions/lang/lb.json b/plugins/Actions/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..c1174e63342aa232d13de77c6dc9cbded85190fa --- /dev/null +++ b/plugins/Actions/lang/lb.json @@ -0,0 +1,56 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "Moyenne bezitt sech op %1$s Opriff %2$s tëscht %3$s an %4$s", + "ColumnClicks": "Klicks", + "ColumnClicksDocumentation": "D'Unzuel vun de Kéiere wei oft de Link geklickt ginn ass.", + "ColumnDownloadURL": "Download Link", + "ColumnEntryPageTitle": "Titel vun der Säit aginn", + "ColumnEntryPageURL": "Link vun der Säit aginn", + "ColumnExitPageTitle": "Aus den Säiten Titel rausgoen", + "ColumnExitPageURL": "Aus den Säiten Link rausgoen", + "ColumnNoResultKeyword": "Stéchwuert ouni Resultater", + "ColumnPageName": "Säiten Numm", + "ColumnPageURL": "Säiten URL", + "ColumnPagesPerSearch": "Sich Resultater Säiten", + "ColumnPagesPerSearchDocumentation": "D'Visiteure wäerten op ärer Internetsäit sichen an heiansdo op \"weider\" klicken, fir méi Resultater ze gesinn. Dat hei ass d'Moyenne vun de Resultater déi fir dëst Stéchwuert gezielt goufen.", + "ColumnSearchCategory": "Sich Kategorie", + "ColumnSearchExitsDocumentation": "De Prozentsätz vun de Visite déi d'Internetsäit , no dem siche vun dësem Stéchwuert an ärer Internetsäit Sichmaschinn, verlooss hunn.", + "ColumnSearchResultsCount": "D'Zuel vun den Sich Resultater", + "ColumnSearches": "Sichen", + "ColumnSearchesDocumentation": "D'Unzuel vun den Visiten dei fir dëst Stéchwuert op ärer Internetsäit Sich gesicht hunn.", + "ColumnSiteSearchKeywords": "Eemoleg Stéchwieder", + "ColumnUniqueClicks": "Eenzel Klicks", + "ColumnUniqueClicksDocumentation": "D'Zuel vun de Visite, déi op dëse Link geklickt hunn. Wann de Link puer mol während enger Visite geklickt gouf, gëtt en nëmmen eemol gezielt.", + "ColumnUniqueDownloads": "Eemoleg Downloads", + "ColumnUniqueOutlinks": "Eemoleg Austrëttslinks", + "DownloadsReportDocumentation": "An dësem Rapport kennt dir gesi wéineg Dokumenter vum Visiteur gedownloat goufen. %s Matomo zielt en Klick op en Download Link als Download. Op den Download fäerdeg gestallt gouf oder net, ass net vu Matomo gewosst.", + "EntryPageTitles": "Antrëtts Säiten Titel", + "EntryPageTitlesReportDocumentation": "Dësen Rapport enthält Informatiounen iwwert d'Titelen vun den Antrëtts Säiten, déi während enger gewesser Period benotzt goufen.", + "ExitPageTitles": "Austrëtts Säiten Titel", + "ExitPageTitlesReportDocumentation": "Desen Rapport enthält Informatiounen iwwert d'Titelen vun den Austrëtts Säiten während enger gewessener Period.", + "OneSearch": "1 Sich", + "OutlinkDocumentation": "En austriedenden Link ass en Link, den den Visiteur vun ärer Internetsäit (op en aneren Domain) leedt.", + "OutlinksReportDocumentation": "Dësen Rapport weist eng hierarchesch Lëscht vun den austriedenden URL'en déi vun ären Visiteuren geklickt goufen.", + "PageTitlesReportDocumentation": "Dësen Rapport enthält Informatiounen iwwert d'Säiten Titelen déi besicht goufen. %1$s Den Säiten Titel ass den HTML %2$s Tag, deen déi meescht Browseren an der Säiten Fënster als Titel uweisen.", + "PageUrls": "Säiten Links", + "PagesReportDocumentation": "Dësen Rapport enthält Informatiounen iwwert d'Säiten URL'en, déi besicht goufen. %s Dës Tabell ass hierarchesch organiséiert, d'URL'en ginn als Dossier Struktur ugewisen.", + "SiteSearchCategories1": "Dësen Rapport lëscht d'Kategorien op déi d'Visiteuren gewielt hunn wann se eppes op ärer Internetsäit gesicht hutt.", + "SiteSearchCategories2": "Zum Beispill: Ecommerce Internetsäiten hunn typescherweis en \"Kategorie\"-Wieler, sou dass d'Visiteuren hir Sich op eng Produiten aus enger spezifescher Kategorie kennen agrenzen.", + "SiteSearchFollowingPagesDoc": "Wann Visiteuren eppes op ärer Internetsäit sichen, dann sichen se no enger bestëmmter Säit, engem bestëmmten Inhalt, Produit oder Service. Dësen Rapport lëscht d'Säiten op, déi am meeschten no enger interner Sich geklickt goufen. An aneren Wieder, d'Lëscht vun den Säiten déi am meeschten vun Visiteuren gesicht goufen, déi schonn op ärer Internetsäit drop waren.", + "SiteSearchKeyword": "Stéchwuert (Säiten Sich)", + "SubmenuPageTitles": "Säiten Titelen", + "SubmenuPagesEntry": "Antrëtts Säiten", + "SubmenuPagesExit": "Austrëtts Säiten", + "SubmenuSitesearch": "Säiten Sich", + "WidgetEntryPageTitles": "Antrëtts Säiten Titelen", + "WidgetExitPageTitles": "Austrëtts Säiten Titelen", + "WidgetPageTitles": "Säiten Titelen", + "WidgetPageTitlesFollowingSearch": "Säiten Titelen no enger Säiten Sich", + "WidgetPageUrlsFollowingSearch": "Säiten no enger Säiten Sich", + "WidgetPagesEntry": "Antrëtts Säiten", + "WidgetPagesExit": "Austrëtts Säiten", + "WidgetSearchCategories": "Sich Kategorien", + "WidgetSearchKeywords": "Säiten Sich Stëchwieder", + "WidgetSearchNoResultKeywords": "Sich Stëchwieder ouni Resultater" + } +} diff --git a/plugins/Actions/lang/lt.json b/plugins/Actions/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..ff995f9bf5d0fc93e1965fb4e6645ebbc249d8e0 --- /dev/null +++ b/plugins/Actions/lang/lt.json @@ -0,0 +1,19 @@ +{ + "Actions": { + "ActionType": "Veiksmo tipas", + "Behaviour": "Elgsena", + "ColumnClicks": "Paspaudimai", + "ColumnDownloadURL": "Atsisiuntimų URL", + "ColumnPageName": "Puslapio pavadinimas", + "ColumnPageURL": "Puslapio URL", + "ColumnSearches": "Paieškos", + "ColumnSiteSearchKeywords": "Unikalūs raktažodžiai", + "ColumnUniqueClicks": "Unikalūs spustelėjimai", + "ColumnUniqueDownloads": "Unikalūs atsisiuntimai", + "PagesSubcategoryHelp3": "Naršymui naudokite kairėje esančias pliuso ir minuso piktogramas.", + "SubmenuPageTitles": "Puslapių antraštės", + "SubmenuPagesEntry": "Užėjimo puslapiai", + "SubmenuPagesExit": "Išėjimo puslapiai", + "WidgetSearchCategories": "Paieškos kategorijos" + } +} diff --git a/plugins/Actions/lang/lv.json b/plugins/Actions/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..cf98c12e9506a2392bb3060f1539918bf5a5e7b5 --- /dev/null +++ b/plugins/Actions/lang/lv.json @@ -0,0 +1,21 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "Vidējais rādījums, balstoties uz %1$s trāpījumu(-iem) %2$s starp %3$s un %4$s", + "ColumnClicks": "Klikšķi", + "ColumnClicksDocumentation": "Saites klikšķu skaits.", + "ColumnDownloadURL": "Lejupielādes URL", + "ColumnEntryPageTitle": "Ieejas lapas virsraksts", + "ColumnEntryPageURL": "Ieejas lapas URL", + "ColumnExitPageTitle": "Izejas lapas virsraksts", + "ColumnExitPageURL": "Izejas lapas URL", + "ColumnPageName": "Lapas virsraksts", + "ColumnPageURL": "Lapas URL", + "ColumnUniqueClicks": "Unikālie klikšķi", + "ColumnUniqueDownloads": "Unikālas lejupielādes", + "ColumnUniqueOutlinks": "Unikālās arējās saites", + "PagesSubcategoryHelp3": "Lai pārvietotos, lietojiet pa kreisi esošās plusa un mīnusa ikonas.", + "SubmenuPageTitles": "Lapas virsraksti", + "SubmenuPagesEntry": "Ieejas lapas", + "SubmenuPagesExit": "Izejas lapas" + } +} diff --git a/plugins/Actions/lang/ms.json b/plugins/Actions/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..fe9871a160e05974d66883aed427b0797b7955f4 --- /dev/null +++ b/plugins/Actions/lang/ms.json @@ -0,0 +1,26 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "Purata berdasarkan %1$s hit(s) %2$s antara %3$s dan %4$s", + "ColumnActionURL": "URL tindakan", + "ColumnClickedURL": "Pautan keluar yang Diklik", + "ColumnClickedURLs": "Pautan Luar Diklik", + "ColumnClicks": "Klik", + "ColumnClicksDocumentation": "Jumlah pautan ini diklik", + "ColumnDownloadURL": "URL Muat Turun", + "ColumnEntryPageTitle": "Tajuk Laman Masuk", + "ColumnEntryPageURL": "URL Laman Masuk", + "ColumnExitPageTitle": "Tajuk Laman Keluar", + "ColumnExitPageURL": "URL Laman Keluar", + "ColumnPageName": "Nama Laman", + "ColumnPageURL": "Laman URL", + "ColumnUniqueClicks": "Klik Unik", + "ColumnUniqueDownloads": "Muat Turun Unik", + "EntryPageTitles": "Tajuk laman masuk", + "ExitPageTitlesReportDocumentation": "Laporan ini mengandungi maklumat mengenai tajuk-tajuk laman keluar yang berlaku dalam tempoh yang ditetapkan.", + "SubmenuPageTitles": "Tajuk laman", + "SubmenuPagesEntry": "Laman masuk", + "SubmenuPagesExit": "Laman keluar", + "WidgetEntryPageTitles": "Tajuk Laman Masuk", + "WidgetExitPageTitles": "Tajuk laman keluar" + } +} diff --git a/plugins/Actions/lang/nb.json b/plugins/Actions/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..d12042a26960b02a90153231dafbccbd1a0c8432 --- /dev/null +++ b/plugins/Actions/lang/nb.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Handlingstype", + "ActionTypes": "Handlingstyper", + "ActionsInVisit": "Handlinger i løpet av besøk", + "AvgGenerationTimeTooltip": "Gjennomsnitt basert på %1$s treff %2$s mellom %3$s og %4$s", + "Behaviour": "Oppførsel", + "ColumnActionURL": "Handlings-URL", + "ColumnClickedURL": "Klikket utgående lenke", + "ColumnClickedURLs": "Klikkede utgående lenker", + "ColumnClicks": "Klikk", + "ColumnClicksDocumentation": "Antall klikk på denne lenken.", + "ColumnDownloadURL": "Nedlastings-URL", + "ColumnDownloadURLs": "Last ned nettadresser", + "ColumnEntryPageTitle": "Tittel på inngangsside", + "ColumnEntryPageURL": "URL på inngangsside", + "ColumnEntryPageURLs": "Inngangsside-nettadresser", + "ColumnExitPageTitle": "Tittel på utgangsside", + "ColumnExitPageURL": "URL på utgangsside", + "ColumnExitPageURLs": "Nettadresser for utgangssider", + "ColumnIdPageview": "Sidevisnings-ID", + "ColumnInteractionPosition": "Interaksjonsposisjon", + "ColumnNoResultKeyword": "Nøkkelord uten søkeresultat", + "ColumnPageName": "Sidenavn", + "ColumnPageURL": "Side-URL", + "ColumnPageViewPosition": "Sidevisningsposisjon", + "ColumnPagesPerSearch": "Søkeresultatsider", + "ColumnPagesPerSearchDocumentation": "Brukere vil søke på ditt nettsted, og noen ganger klikke «neste» for å se flere resultater. Dette er det gjennomsnittlige antallet søkeresultatsider for dette nøkkelordet.", + "ColumnSearchCategory": "Søkekategori", + "ColumnSearchExits": "%% søkeutganger", + "ColumnSearchExitsDocumentation": "Antall besøkende som forlot nettsiden etter å ha søkt etter dette nøkkelordet.", + "ColumnSearchResultsCount": "Antall søkeresultater", + "ColumnSearches": "Søk", + "ColumnSearchesDocumentation": "Antall besøkende som søkte etter dette nøkkelordet i ditt nettsteds søkemotor.", + "ColumnSiteSearchKeywords": "Unike nøkkelord", + "ColumnUniqueClicks": "Unike klikk", + "ColumnUniqueClicksDocumentation": "Antall besøk som involverte et klikk på denne lenken. Hvis lenken ble klikket flere ganger i løpet av et besøk, blir det kun talt én gang.", + "ColumnUniqueDownloads": "Unike nedlastinger", + "ColumnUniqueOutlinks": "Unike utlenker", + "DownloadsReportDocumentation": "I denne rapporten kan du se hvilke filer de besøkende har lastet ned. %s Hva Matomo teller som en nedlasting, er klikket på en nedlastingslink. Om nedlastingen ble fullført eller ikke er ikke kjent for Matomo.", + "DownloadsSubcategoryHelp1": "I denne rapporten kan du se hvilke file dine besøkende har lastet ned.", + "DownloadsSubcategoryHelp2": "Det Matomo teller som en nedlasting er et klikk på en nedlastingslenke. Hvorvidt nedlastingen ble fullført eller ei vites ikke av Matomo.", + "EntryPageTitles": "Titler på inngangssider", + "EntryPageTitlesReportDocumentation": "Denne rapporten inneholder informasjon om titler på inngangssider som ble brukt i løpet av den angitte perioden.", + "EntryPagesReportDocumentation": "Denne rapporten inneholder info om inngangssidene som ble brukt i den angitte perioden. En inngangsside er den første siden en bruker møtes med under første besøk. %s Inngangs-nettadresser vises som en mappestruktur.", + "EntryPagesSubcategoryHelp1": "Rapporten inneholder info om inngangssider som ble brukt innenfor en angitt periode. En inngangsside er den første siden en bruker møtes med under første besøk.", + "EntryPagesSubcategoryHelp2": "Inngangsnettadresser vises som en mappestruktur.", + "ExitPageTitles": "Titler på utgangssider", + "ExitPageTitlesReportDocumentation": "Denne rapporten inneholder informasjon om titler på utgangssider som ble brukt i den angitte perioden.", + "ExitPagesReportDocumentation": "Denne rapporten inneholder info om utgangssidene som ble brukt i den angitte perioden. En utgangsside er den siste siden en bruker brukte under sitt besøk. %s Utgangs-nettadresser vises som en mappestruktur.", + "ExitPagesSubcategoryHelp1": "Denne rapporten inneholder info om utgangssider som inntraff innenfor den angitte perioden. En avsluttningsside er den siste siden en bruker brukte i løpet av besøket sitt.", + "ExitPagesSubcategoryHelp2": "Utgangsnettadresser viser som en mappestruktur.", + "MainMetricsReportDocumentation": "Denne rapporten gir en veldig grunnleggende oversikt over hvilke foretagende dine besøkende utførte på nettsiden din.", + "OneSearch": "1 søk", + "OutlinkDocumentation": "En utlenke er en lenke som leder besøkeren bort fra ditt nettsted (til et annet domene).", + "OutlinksReportDocumentation": "Denne rapporten viser en hierarkisk liste av utlenkers URL-er som ble klikket av dine besøkere.", + "OutlinksSubcategoryHelp1": "Denne rapporten viser en hierarkisk liste over utlenke-nettadresser som ble klikket av dine besøkende. En utlenke er en lenke som leder den besøkende bort fra nettsiden din (til et annet domene).", + "PageTitlesReportDocumentation": "Denne rapporten inneholder informasjon om titler på sider som har blitt besøkt. %1$s Sidetittelen er HTML-taggen %2$s som de fleste nettlesere viser i vindustittelen.", + "PageTitlesSubcategoryHelp1": "Denne rapporten inneholder info om navnene på sider som har blitt besøkt.", + "PageTitlesSubcategoryHelp2": "Sidens navn er en HTML-&lt;title&gt;-tagg de fleste nettlesere viser i tittellinjen.", + "PageUrls": "Side-URLer", + "PageViewsByVisitor": "Antall ganger denne siden ble besøkt av denne besøkeren i løpet av alle besøk", + "PagesReportDocumentation": "Denne rapporten inneholder informasjon om side-URLer som har blitt besøkt. %s Tabellen er organisert hierarkisk, URL-ene vises som en mappestruktur.", + "PagesSubcategoryHelp1": "Denne rapporten inneholder info om besøkte nettadresser på siden.", + "PagesSubcategoryHelp2": "Tabellen er organisert i et hierarki. Nettadresser vises som en mappestruktur.", + "PagesSubcategoryHelp3": "Bruk pluss- og minus-ikonene til venstre for å navigere.", + "PluginDescription": "Rapporter om sidevisninger org sidetitler. Lar deg måle ditt nettsteds interne søkemotor. Sporer klikk på eksterne lenker og nedlastinger automatisk.", + "RevisitedPages": "Sider besøkt mer enn én gang", + "SiteSearchCategories": "Kategorier (sidesøk)", + "SiteSearchCategories1": "Denne rapporten viser kategorier som besøkere velger når de søker på ditt nettsted.", + "SiteSearchCategories2": "For eksempel: e-handelsnettsteder har typisk en «kategori»-velger slik at besøkere kan begrense sine søk til kun produkter i en spesifikk kategori.", + "SiteSearchCategory": "Kategori (sidesøk)", + "SiteSearchFollowingPagesDoc": "Når besøkere søker på ditt nettsted, leter de etter en bestemt side, innhold, produkt eller tjeneste. Denne rapporten lister opp sider som ble klikket mest etter et internt søk. Med andre ord, det er listen over sider som blir mest søkt etter av besøkere som allerede er på nettstedet.", + "SiteSearchIntro": "Å spore søk som besøkere gjør på dine nettsteder er en effektiv måte å lære mer om hva din målgruppe leter etter. Det kan hjelpe deg å finne ideer til nytt innhold, nye produkter og generelt å øke kvaliteten på dine besøkeres opplevelse av ditt nettsted.", + "SiteSearchKeyword": "Nøkkelord (Sidesøk)", + "SiteSearchKeywordCount": "Antall søkeord (søk på siden)", + "SiteSearchKeywordCounts": "Antall nøkkelord (søk på siden)", + "SiteSearchKeywords": "Nøkkelord (sidesøk)", + "SiteSearchKeywordsDocumentation": "Denne rapporten lister opp nøkkelord som besøkere har søkt etter i nettstedets interne søkemotor.", + "SiteSearchKeywordsNoResultDocumentation": "Denne rapporten lister opp nøkkelord som ikke ga noen søkeresultater. Kanskje søkealgoritmen kan forbedres, eller kanskje dine besøkere ser etter innhold som ikke er på ditt nettsted (ennå)?", + "SiteSearchSubcategoryHelp1": "Sidesøksdelen viser hvilke nøkkelord besøkende bruker når de søker på nettsiden din. Den viser også hvilke sider brukere går til etter å ha utført et søk og hvilke søkeord som er resultatløse.", + "SiteSearchSubcategoryHelp2": "Disse rapportene kan gi deg idéer om manglende innhold på siden din, innsikt i hva dine besøkende ser etter og ikke finner, med mer.", + "SiteSearchSubcategoryHelp3": "Lær mer i veiledningen for søk på siden.", + "SubmenuPageTitles": "Sidetitler", + "SubmenuPagesEntry": "Inngangssider", + "SubmenuPagesExit": "Utgangssider", + "SubmenuSitesearch": "Sidesøk", + "TimeSpentInReferringAction": "Tid brukt i henvisningsforetagende", + "WidgetEntryPageTitles": "Titler på inngangssider", + "WidgetExitPageTitles": "Titler på utgangssider", + "WidgetPageTitles": "Sidetitler", + "WidgetPageTitlesFollowingSearch": "Sidetittel fulgt av ett nettstedssøk", + "WidgetPageUrlsFollowingSearch": "Sider som følger etter et nettstedssøk", + "WidgetPagesEntry": "Inngangssider", + "WidgetPagesExit": "Utgangssider", + "WidgetSearchCategories": "Søkekategorier", + "WidgetSearchKeywords": "Nøkkelord for sidesøk", + "WidgetSearchNoResultKeywords": "Nøkkelord uten resultater" + } +} diff --git a/plugins/Actions/lang/nl.json b/plugins/Actions/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..025874a90d089bc155a3d1be463813f11976f286 --- /dev/null +++ b/plugins/Actions/lang/nl.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Soort actie", + "ActionTypes": "Soorten acties", + "ActionsInVisit": "Acties tijdens bezoek", + "AvgGenerationTimeTooltip": "Gemiddelde gebaseerd op %1$s hit(s) %2$s tussen %3$s en %4$s", + "Behaviour": "Gedrag", + "ColumnActionURL": "Actie-URL", + "ColumnClickedURL": "Aangeklikte uitgaande link", + "ColumnClickedURLs": "Aangeklikte uitgaande links", + "ColumnClicks": "Kliks", + "ColumnClicksDocumentation": "Aantal keren dat er op deze link was gekikt.", + "ColumnDownloadURL": "Download-URL", + "ColumnDownloadURLs": "Download-URL's", + "ColumnEntryPageTitle": "Titel binnenkomstpagina", + "ColumnEntryPageURL": "URL binnenkomstpagina", + "ColumnEntryPageURLs": "URL's binnenkomstpagina's", + "ColumnExitPageTitle": "Titel vertrekpagina", + "ColumnExitPageURL": "URL vertrekpagina", + "ColumnExitPageURLs": "URL's vertekpagina's", + "ColumnIdPageview": "Paginaweergave-ID", + "ColumnInteractionPosition": "Positie interactie", + "ColumnNoResultKeyword": "Sleutelwoord zonder zoekresultaat", + "ColumnPageName": "Naam pagina", + "ColumnPageURL": "Pagina-URL", + "ColumnPageViewPosition": "Paginaweergave positie", + "ColumnPagesPerSearch": "Zoekresultaten-pagina's", + "ColumnPagesPerSearchDocumentation": "Bezoekers zoeken op uw website en klikken soms op \"volgende\" voor meer zoekresultaten. Dit is het gemiddeld aantal zoekreultatenpagina's dat voor dit sleutelwoord is bekeken.", + "ColumnSearchCategory": "Zoekcategorie", + "ColumnSearchExits": "%% verlaten na zoekopdracht", + "ColumnSearchExitsDocumentation": "Het percentage bezoeken dat uw website heeft verlaten nadat op dit sleutelwoord werd gezocht met de zoekoptie van uw website.", + "ColumnSearchResultsCount": "Aantal zoekresultaten", + "ColumnSearches": "Zoekopdrachten", + "ColumnSearchesDocumentation": "Aantal bezoeken dat op dit sleutelwoord heeft gezocht via de zoekmachine van uw website.", + "ColumnSiteSearchKeywords": "Unieke sleutelwoorden", + "ColumnUniqueClicks": "Unieke kliks", + "ColumnUniqueClicksDocumentation": "Het aantal bezoeken dat een klik op deze link betrokken. Als een link is geklikt meerdere keren tijdens een bezoek, is het maar een keer geteld.", + "ColumnUniqueDownloads": "Unieke downloads", + "ColumnUniqueOutlinks": "Unieke uitgaande links", + "DownloadsReportDocumentation": "Dit rapport laat zien welke bestanden door de bezoekers zijn gedownload. %s Matomo telt een klik op een downloadlink als een download. Matomo kan niet zien of de download succesvol is uitgevoerd en afgerond.", + "DownloadsSubcategoryHelp1": "In dit rapport ziet u welke bestanden bezoekers hebben gedownload.", + "DownloadsSubcategoryHelp2": "Wat Matomo telt als een download is de klik op een download link. Of de download voltooid werd is niet bekend door Matomo.", + "EntryPageTitles": "Binnenkomende paginatitels", + "EntryPageTitlesReportDocumentation": "Dit rapport bevat informatie over de titels van de inkomende pagina's in de opgegeven periode.", + "EntryPagesReportDocumentation": "Dit rapport bevat informatie over de landingpagina's van een bezoeker welke bezocht zijn gedurende een specifieke periode. Een landingspagina is de eerste pagina die bekeken wordt tijdens een bezoek. %s. The landingspagina's URL's worden getoond in een folder structuur.", + "EntryPagesSubcategoryHelp1": "Dit rapport toont informatie over ingangspagina's die gebruikt werden gedurende de opgegeven periode. Een ingangspagina is de eerste pagina die een gebruiker bekijkt tijdens diens bezoek.", + "EntryPagesSubcategoryHelp2": "De ingang-URL's worden weergegeven als mappenstructuur.", + "ExitPageTitles": "Titels vertrekpagina's", + "ExitPageTitlesReportDocumentation": "Dit rapport bevat informatie over de titels van de uitgaande pagina's in de opgegeven periode.", + "ExitPagesReportDocumentation": "Dit rapport bevat informatie over de exit pagina's gedurende een geselecteerde periode. Een exit pagina is de laatste pagina die bekeken is gedurende een bezoek. %s De exit pagina's worden getoond in een folder structuur.", + "ExitPagesSubcategoryHelp1": "Dit rapport toont informatie over de uitgangspagina's die voorkwamen gedurende de opgegeven periode. Een uitgangspagina is de laatste pagina die een gebruiker bekijkt bij diens bezoek.", + "ExitPagesSubcategoryHelp2": "De uitgang-URL's worden weergegeven als mappenstructuur.", + "MainMetricsReportDocumentation": "Dit rapport geeft een zeer eenvoudig overzicht over welke activiteiten bezoekers uitvoeren op uw website.", + "OneSearch": "1 zoekopdracht", + "OutlinkDocumentation": "Een uitgaande link is een link die de bezoeker wegleid van uw website (naar een ander domein).", + "OutlinksReportDocumentation": "Dit rapport geeft een hiërarchische lijst van uitgaande link URL's die zijn aangeklikt door uw bezoekers.", + "OutlinksSubcategoryHelp1": "Dit rapport toont een hiërarchische lijst van uitgaande link URL's die aangeklikt werden door bezoekers. Een uitgaande link is een link die de bezoeker wegleidt van uw website (naar een ander domein).", + "PageTitlesReportDocumentation": "Dit rapport bevat informatie over de titels van de pagina's die zijn bezocht. %1$s De paginatitel is de HTML %2$s Tag dat de meeste browsers laten zien in hun window titel.", + "PageTitlesSubcategoryHelp1": "Dit rapport toont informatie over de titels van de bezochte pagina's.", + "PageTitlesSubcategoryHelp2": "De paginatitel is de HTML &lt;title&gt; tag die de meeste browsers tonen als titel in hun venster.", + "PageUrls": "Pagina-URL's", + "PageViewsByVisitor": "Aantal keer dat deze pagina door deze bezoeker tijdens al diens bezoeken bekeken is", + "PagesReportDocumentation": "Dit rapport bevat informatie over de pagina-URL's die zijn bezocht. %s De tabel is hiërarchisch georganiseerd. De URL's worden weergegeven in een map structuur.", + "PagesSubcategoryHelp1": "Dit rapport toont informatie over de bezochte pagina URL's.", + "PagesSubcategoryHelp2": "De tabel is hiërarchisch georganiseerd, de URL's worden als mappenstructuur weergegeven.", + "PagesSubcategoryHelp3": "Gebruik de + en - icoontjes aan de linkerzijde om te navigeren.", + "PluginDescription": "Rapporteert over pagina weergaven en pagina titels. Het maakt het interne zoekgedrag op uw website inzichtelijk en volgt automatisch het klikken op externe linken en downloads van bestanden.", + "RevisitedPages": "Pagina's die meer dan eens zijn bekeken", + "SiteSearchCategories": "Categorieën (zoeken via site)", + "SiteSearchCategories1": "Dit rapport toont de categorieën die bezoekers hebben geselecteerd bij het zoeken op uw website.", + "SiteSearchCategories2": "Bijvoorbeeld, Ecommerce websites hebben vaak een categoriekeuze waarbij de zoekresultaten worden beperkt tot een specifieke categorie.", + "SiteSearchCategory": "Categorie (zoeken via site)", + "SiteSearchFollowingPagesDoc": "Als bezoekers op uw website surfen, dan zoekt men vaak een bepaalde pagina, inhoud, product of dienst. Dit rapport laat de pagina's zien die het vaakst zijn aangeklikt na een interne zoekopdracht. Met andere woorden: de meeste gezochte pagina's door bezoekers die reeds op uw website zijn.", + "SiteSearchIntro": "Het volgen van het zoekgedrag dat bezoekers op uw website vertonen is een zeer effectieve manier om er achter te komen wat het publiek op uw website zoekt. Het kan u aan nieuwe ideeën helpen om nieuwe content te maken of nieuwe Ecommerceprodukten toe te passen die helpen om de bezoekersbeleving op uw website te verhogen.", + "SiteSearchKeyword": "Sleutelwoord (zoeken via site)", + "SiteSearchKeywordCount": "Aantal sleutelwoorden (zoeken via de site)", + "SiteSearchKeywordCounts": "Aantal sleutelwoorden (zoeken via de site)", + "SiteSearchKeywords": "Sleutelwoorden (zoeken via site)", + "SiteSearchKeywordsDocumentation": "Dit rapport toont een lijst met sleutelwoorden waarop bezoekers hebben gezocht met de interne zoekoptie van uw website.", + "SiteSearchKeywordsNoResultDocumentation": "Dit rapport toont een lijst met sleutelwoorden waarvoor uw website geen zoekresultaten kon gegeven. Wellicht kan het zoekalgoritme worden verbeterd of misschien wordt er gezocht op content die (nog) niet beschikbaar is op uw website?", + "SiteSearchSubcategoryHelp1": "De Site zoekopdrachten sectie toont welke sleutelwoorden bezoekers gebruikten bij het zoeken op uw website. Het toont ook welke pagina's gebruikers bekijken na de zoekopdracht en welke sleutelwoorden geen enkel resultaat opleveren.", + "SiteSearchSubcategoryHelp2": "Deze rapporten geven u een idee over ontbrekende inhoud op uw website, inzicht naar wat bezoekers zoeken maar niet kunnen vinden, en meer.", + "SiteSearchSubcategoryHelp3": "Lees meer in de handleiding Site Search.", + "SubmenuPageTitles": "Paginatitels", + "SubmenuPagesEntry": "Binnenkomstpagina's", + "SubmenuPagesExit": "Vertrekpagina's", + "SubmenuSitesearch": "Lokale zoekopdracht", + "TimeSpentInReferringAction": "Tijd besteed aan doorverwijzing", + "WidgetEntryPageTitles": "Binnenkomende paginatitels", + "WidgetExitPageTitles": "Uitgaande paginatitels", + "WidgetPageTitles": "Paginatitels", + "WidgetPageTitlesFollowingSearch": "Paginatitels na zoekopdrachten", + "WidgetPageUrlsFollowingSearch": "Bestemmingspagina na zoekopdracht", + "WidgetPagesEntry": "Binnenkomstpagina's", + "WidgetPagesExit": "Vertrekpagina's", + "WidgetSearchCategories": "Zoekcategorieën", + "WidgetSearchKeywords": "Zoektermen", + "WidgetSearchNoResultKeywords": "Zoekwoorden zonder resultaten" + } +} diff --git a/plugins/Actions/lang/nn.json b/plugins/Actions/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..47639f4402422e989bfa559c11f8e86c37f8998f --- /dev/null +++ b/plugins/Actions/lang/nn.json @@ -0,0 +1,31 @@ +{ + "Actions": { + "ColumnClicks": "Klikk", + "ColumnClicksDocumentation": "Talet på klikk på denne peikaren.", + "ColumnDownloadURL": "Nedlasta URL", + "ColumnEntryPageTitle": "Tittel til inngangsside", + "ColumnEntryPageURL": "URL til inngangsside", + "ColumnExitPageTitle": "Tittel til utgangsside", + "ColumnExitPageURL": "URL til utgangsside", + "ColumnPageName": "Sidenamn", + "ColumnPageURL": "Side-URL", + "ColumnUniqueClicks": "Unike klikk", + "ColumnUniqueClicksDocumentation": "Talet på vitjingar som involverte eit klikk på denne peikaren. Viss ein peikar vart klikka fleire gonger under ei vitjing, reknast det med berre ein gong.", + "ColumnUniqueDownloads": "Unike nedlastingar", + "ColumnUniqueOutlinks": "Unike utpeikarar", + "DownloadsReportDocumentation": "I denne rapporten kan du sjå kva filer som vitjarane har lasta ned. %s Matomo tel berre klikk på peikarar til filer. Det er ikkje kjent for Matomo om nedlastinga vart fullførd eller ikkje.", + "EntryPageTitles": "Tittel på inngangssider", + "EntryPageTitlesReportDocumentation": "Denne rapporten inneheld informasjon om sidetittelen til inngangssider gjennom ein oppgjeven periode.", + "ExitPageTitles": "Tittel på utgangssider", + "ExitPageTitlesReportDocumentation": "Denne rapporten inneheld informasjon om sidetittelen til utgangssider gjennom ein oppgjeven periode.", + "OutlinkDocumentation": "Ein utpeikar er ein peikar som leier vitjarane dine vekk frå nettsida di (til eit anna domene).", + "OutlinksReportDocumentation": "Denne rapporten viser ei hierarkisk liste av utpeikarar som vitjarane dine klikka på.", + "PageTitlesReportDocumentation": "Denne rapporten inneheld informasjon om titlane til sidene som er vitja. %1$s Sidetittelen er HTML-entiteten %2$s som dei fleste nettlesarane viser som tittel på vindauget sitt.", + "PageUrls": "Side-URLar", + "PagesReportDocumentation": "Denne rapporten inneheld informasjon om side-URLar som er vitja. %s Tabellen er organisert hierarkisk, URLane er vist som ein mappestruktur.", + "PagesSubcategoryHelp3": "Bruk pluss- og minusikonane til venstre for å navigera.", + "SubmenuPageTitles": "Sidetitlar", + "SubmenuPagesEntry": "Inngangssider", + "SubmenuPagesExit": "Utgangssider" + } +} diff --git a/plugins/Actions/lang/pl.json b/plugins/Actions/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..8ec56ffc84b91e55140a48af1e29a01d25dc4080 --- /dev/null +++ b/plugins/Actions/lang/pl.json @@ -0,0 +1,81 @@ +{ + "Actions": { + "ActionType": "Typ Akcji", + "ActionTypes": "Typy Akcji", + "ActionsInVisit": "Akcji podczas wizyty", + "AvgGenerationTimeTooltip": "Średnia podstawa dla %1$s trafień %2$s pomiędzy %3$s i %4$s", + "Behaviour": "Zachowanie", + "ColumnActionURL": "Adres URL Akcji", + "ColumnClickedURL": "Kliknięcie Linku Zewnętrznego", + "ColumnClickedURLs": "Kliknięte Linki zewnętrzne", + "ColumnClicks": "Kliknięcia", + "ColumnClicksDocumentation": "Ilość kliknięć na ten link.", + "ColumnDownloadURL": "Adres URL pobrań", + "ColumnDownloadURLs": "Adresy pobrań", + "ColumnEntryPageTitle": "Tytuł stron z których wchodzono", + "ColumnEntryPageURL": "Adres strony wejścia", + "ColumnEntryPageURLs": "Adresy stron wejścia", + "ColumnExitPageTitle": "Tytuł strony wybierany przy opuszczaniu strony", + "ColumnExitPageURL": "Adres URL wybierany przy opuszczaniu strony", + "ColumnExitPageURLs": "Adres strony wyjścia", + "ColumnIdPageview": "ID wyświetlenia strony", + "ColumnInteractionPosition": "Miejsce interakcji", + "ColumnNoResultKeyword": "Brak wyników dla podanego słowa kluczowego", + "ColumnPageName": "Nazwa strony", + "ColumnPageURL": "Adres URL strony", + "ColumnPagesPerSearch": "Strony z Wynikami Wyszukiwania", + "ColumnPagesPerSearchDocumentation": "Odwiedzający będą wyszukiwali na twojej stronie i czasem klikną \"dalej\" aby zobaczyć więcej wpisów. To jest średnia liczba wyszukiwań widoczna dla tego słowa kluczowego.", + "ColumnSearchCategory": "Kategoria wyszukiwania", + "ColumnSearchExits": "%% wyjść z wyszukiwania", + "ColumnSearchExitsDocumentation": "Procent odwiedzających którzy zostawili stronę dla danego słowa Kluczowego używając twojego systemu wyszukiwania.", + "ColumnSearchResultsCount": "Liczba wyników wyszukiwania", + "ColumnSearches": "Wyszukiwania", + "ColumnSearchesDocumentation": "Liczba wizyt, która wyszukiwała tego słowa kluczowego w wyszukiwarce na Twojej stronie.", + "ColumnSiteSearchKeywords": "Unikalne słowa kluczowe", + "ColumnUniqueClicks": "Kliknięcia bez powtórzeń", + "ColumnUniqueClicksDocumentation": "Liczba odwiedzających którzy kliknęli na ten link. Jeśli link był kliknięty wielokrotnie podczas wizyty, wtedy liczony jest tylko raz.", + "ColumnUniqueDownloads": "Pobrania bez powtórzeń", + "ColumnUniqueOutlinks": "Unikalne linki zewnętrzne", + "DownloadsReportDocumentation": "W tym raporcie możesz zobaczyć które pliki twoi odwiedzający pobrali. %s Które Matomo liczy jako pobrane kliknięcia na link pobrania. Niezalerznie czy pobranie było ukończone lub czy Matomo nie ma o tym informacji.", + "EntryPageTitles": "Tytuły stron Wejściowych", + "EntryPageTitlesReportDocumentation": "Ten raport zawiera informację o tytułach stron wejściowych które były używane podczas wybranego okresu.", + "EntryPagesReportDocumentation": "Ten raport zawiera informacje o stronach wejścia użytych w określonym przedziale czasu. Strona wejściowa, to pierwsza strona, którą odwiedzający zobaczy podczas wizyty. %s Adresy wejścia są wyświetlane w strukturze katalogów.", + "ExitPageTitles": "Tytuły stron Wyjść", + "ExitPageTitlesReportDocumentation": "Ten raport zawiera informację o tytułach stron wyjściowych, które wystąpiły podczas wybranego okresu.", + "ExitPagesReportDocumentation": "Ten raport zawiera informacje o stronach wyjścia, które wystąpiły w określonym przedziale czasowym. Strona wyjściowa to ostatnia strona, którą odwiedzający oglądał podczas wizyty. %s Adresy stron wyjścia wyświetlane są w strukturze katalogów.", + "OneSearch": "1 wyszukiwanie", + "OutlinkDocumentation": "Link wyjściowy to link który prowadzi odwiedzającego poza twoją stronę (do innej domeny).", + "OutlinksReportDocumentation": "Ten raport pokazuje ustrukturyzowaną listę linków wychodzących które zostały kliknięte przez odwiedzających.", + "PageTitlesReportDocumentation": "Ten raport zawiera informację o tytułach stron które zostały odwiedzone. %1$s Tytuł stron to %2$s znacznik HTML które większość przeglądarek wyświetla na belce tytułowej okna.", + "PageUrls": "URL strony", + "PageViewsByVisitor": "Łączna liczba wyświetleń tej strony przez odwiedzającego podczas wizyt", + "PagesReportDocumentation": "Ten raport zawiera informację o URL-ach stron które zostały odwiedzone. %s Tabela jest organizowana w hierarchii, URL-e są wyświetlane z uwzględnieniem struktury katalogów.", + "PagesSubcategoryHelp3": "Użyj ikony plus i minus po lewej stronie nawigacji.", + "PluginDescription": "Raporty dotyczące odwiedzin i tytułów stron. Pozwala na analizę działania wewnętrznej wyszukiwarki serwisu. Automatycznie śledzi kliknięcia na linkach zewnętrznych i pobraniach plików.", + "RevisitedPages": "Strony wyświetlone więcej niż raz", + "SiteSearchCategories": "Kategorie (Wyszukiwarka)", + "SiteSearchCategories1": "Ten raport wyświetla listę Kategorii które odwiedzający wybrali kiedy używali wyszukiwarki na twojej stronie.", + "SiteSearchCategories2": "Na przykład, strony E-Commerce zazwyczaj mają selektor \"Kategoria\" dzięki czemu odwiedzający mogą ograniczyć wyszukiwanie wszystkich produktów dla danej kategorii.", + "SiteSearchCategory": "Kategoria (Wyszukiwarka)", + "SiteSearchFollowingPagesDoc": "Kiedy odwiedzający wyszukują na twojej stronie, wtedy szukają danej strony, zawartości, produktu lub usługi. Ten raport zawiera listę stron, które najczęściej były klikane po wewnętrznym wyszukiwaniu. Innymi słowy, lista stron które najczęściej użytkownicy szukają jak już znajdą się na twoje stronie.", + "SiteSearchIntro": "Śledzenie wyszukiwań jakie odwiedzający wykonują na twojej stronie jest bardzo efektywnym sposobem poznania więcej informacji o tym czego publiczność szuka, może on pomóc znaleźć pomysły na nową zawartość, nowy produkt E-Commercem, który klienci będą szukać oraz generalnie poprawić doświadczenia odwiedzających twoją stronę.", + "SiteSearchKeyword": "Słowo kluczowe (Wyszukiwarka)", + "SiteSearchKeywords": "Słowa kluczowe (Wyszukiwarka)", + "SiteSearchKeywordsDocumentation": "Raport ten przedstawia listę Słów Kluczowych, które odwiedzający wyszukiwali używając wyszukiwarki serwisu.", + "SiteSearchKeywordsNoResultDocumentation": "Ten raport jest to lista Słów Kluczowych które nie zwróciły żadnych wyników: może algorytm wyszukiwarki może być poprawiony, z może twoi odwiedzający szukają treści jakiej (jeszcze) nie ma na twojej stronie?", + "SubmenuPageTitles": "Tytuły stron", + "SubmenuPagesEntry": "Strony wejść", + "SubmenuPagesExit": "Strony wyjść", + "SubmenuSitesearch": "Wyszukiwarka", + "WidgetEntryPageTitles": "Tytuły Stron Wejściowych", + "WidgetExitPageTitles": "Tytuły Stron Wyjściowych", + "WidgetPageTitles": "Tytuły stron", + "WidgetPageTitlesFollowingSearch": "Tytuły Stron po Wyszukiwaniu Strony", + "WidgetPageUrlsFollowingSearch": "Strony Występujące po Wyszukiwaniu Stron", + "WidgetPagesEntry": "Strony wejściowe", + "WidgetPagesExit": "Strony wyjściowe", + "WidgetSearchCategories": "Kategorie wyszukiwań", + "WidgetSearchKeywords": "Słowa kluczowe wyszukiwarki", + "WidgetSearchNoResultKeywords": "Słowa Kluczowe bez Wyników" + } +} diff --git a/plugins/Actions/lang/pt-br.json b/plugins/Actions/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..ce20d3d0a44c06edb514ffbda538f31ac7447961 --- /dev/null +++ b/plugins/Actions/lang/pt-br.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Tipo de Ação", + "ActionTypeSegmentHelp": "Um tipo de ação, como: %s", + "ActionTypes": "Tipo de Ação", + "ActionsInVisit": "Ações em Visita", + "AvgGenerationTimeTooltip": "Média baseada em %1$s visita(s) %2$s entre %3$s e %4$s", + "Behaviour": "Comportamento", + "ColumnActionURL": "URL Ação", + "ColumnClickedURL": "Outlink clicado", + "ColumnClickedURLs": "Outlinks clicados", + "ColumnClicks": "Cliques", + "ColumnClicksDocumentation": "O número de vezes que link foi clicado.", + "ColumnDownloadURL": "URL do Download", + "ColumnDownloadURLs": "URLs de download", + "ColumnEntryPageTitle": "Título da página de entrada", + "ColumnEntryPageURL": "URL da página de entrada", + "ColumnEntryPageURLs": "URLs de página de entrada", + "ColumnExitPageTitle": "Título da página de Saída", + "ColumnExitPageURL": "URL da página de saída", + "ColumnExitPageURLs": "URLs de páginas de saída", + "ColumnIdPageview": "ID da página", + "ColumnInteractionPosition": "Posição de Interação", + "ColumnNoResultKeyword": "Palavra-chave com Nenhum resultado na busca", + "ColumnPageName": "Nome da página", + "ColumnPageURL": "URL da página", + "ColumnPageViewPosition": "Posição de exibição de página", + "ColumnPagesPerSearch": "Resultados das páginas de pesquisa", + "ColumnPagesPerSearchDocumentation": "Os visitantes poderão pesquisar em seu site, e às vezes clicar em \"próximo\" para ver mais resultados. Este é o número médio de páginas de resultados de pesquisa vistos para esta palavra-chave.", + "ColumnSearchCategory": "Categoria de Pesquisa", + "ColumnSearchExits": "1%% Pesquisa Existe", + "ColumnSearchExitsDocumentation": "A porcentagem de visitas que deixou o site após a pesquisar esta palavra-chave pelo motor de busca do seu site.", + "ColumnSearchResultsCount": "Contagem dos Resultados da Pesquisa", + "ColumnSearches": "Pesquisas", + "ColumnSearchesDocumentation": "O número de visitas que procuraram por esta palavra-chave no motor de busca do seu site.", + "ColumnSiteSearchKeywords": "Palavras-chave únicas", + "ColumnUniqueClicks": "Cliques únicos", + "ColumnUniqueClicksDocumentation": "O número de visitas que envolveram um click neste link. Se um link foi clicado várias vezes durante uma visita é registrado somente uma vez.", + "ColumnUniqueDownloads": "Downloads únicos", + "ColumnUniqueOutlinks": "Outlinks únicos", + "DownloadsReportDocumentation": "Neste relatório, você pode ver quais arquivos seus visitantes baixaram. %s Matomo registra como download um click em um link de download. Se o download foi completado ou não o Matomo não consegue determinar.", + "DownloadsSubcategoryHelp1": "Neste relatório, você pode ver quais arquivos seus visitantes baixaram.", + "DownloadsSubcategoryHelp2": "O que o Matomo conta como descarga é o clique no link para baixar. Se a descarga foi concluída ou não, o Matomo não sabe.", + "EntryPageTitles": "Título de páginas de entradas", + "EntryPageTitlesReportDocumentation": "Este relatório contém informações sobre os títulos das páginas de entrada que foram utilizados durante o período especificado.", + "EntryPagesReportDocumentation": "Este relatório contém informações sobre as páginas de entrada que foram usadas durante o período especificado. Uma página de entrada é a primeira página que um usuário visualiza durante a visita. 1%s As URLs de entrada são exibidos como uma estrutura de pastas.", + "EntryPagesSubcategoryHelp1": "Este relatório contém informação sobre as páginas de entrada que foram usadas durante o período específico. Uma página de entrada é a primeira página que um usuário visualiza durante sua visita.", + "EntryPagesSubcategoryHelp2": "As URLs de entrada são exibidas como uma estrutura de pastas.", + "ExitPageTitles": "Título de páginas de saída", + "ExitPageTitlesReportDocumentation": "Este relatório contém informações sobre os títulos de páginas de saída que ocorreram durante o período especificado.", + "ExitPagesReportDocumentation": "Este relatório contém informações sobre as páginas de saída que ocorreram durante o período especificado. Uma página de saída é a última página que um usuário visualiza durante a visita. 1%s As URLs de saída são exibidos como uma estrutura de pastas.", + "ExitPagesSubcategoryHelp1": "Este relatório contém informação sobre as páginas de saída que ocorrem durante o período específico, Uma pagina de saída é a última página que o usuário visualiza durante sua visita.", + "ExitPagesSubcategoryHelp2": "As URLs de saída são exibidas como uma estrutura de pastas.", + "MainMetricsReportDocumentation": "Este relatório fornece uma visão geral muito básica de quais ações seus visitantes realizam em seu site.", + "OneSearch": "1 pesquisa", + "OutlinkDocumentation": "Um outlink é um link que leva o visitante para fora do seu site (para outro domínio).", + "OutlinksReportDocumentation": "Este relatório mostra uma lista hierárquica de URLs de outlinks clicadas pelos seus visitantes.", + "OutlinksSubcategoryHelp1": "Este relatório mostra uma lista hierárquica de URLs de links externos que foram clicados por seus visitantes. Um link externo é um link que leva o visitante para fora do seu site (para outro domínio).", + "PageTitlesReportDocumentation": "Este relatório contém informações sobre títulos de páginas as quais foram visitadas. %1$s O título da página é um HTML %2$s Tag a qual é mais visualizada nos títulos do browser.", + "PageTitlesSubcategoryHelp1": "Este relatório contém informação sobre os títulos das páginas que foram visitadas.", + "PageTitlesSubcategoryHelp2": "O título da página é a tag HTML &lt;title&gt; que a maioria dos navegadores mostram em seu título da janela.", + "PageUrls": "URLs de Página(s)", + "PageViewsByVisitor": "Número de vezes que esta página foi visualizada por este visitante em todas as visitas", + "PagesReportDocumentation": "Este relatório contém informações sobre URL de páginas as quais foram visitadas. %s A tabela está organizada hierarquicamente, as URLs estão apresentadas como estrutura de pastas.", + "PagesSubcategoryHelp1": "Este relatório contém informação sobre as URLs das páginas que foram visitadas.", + "PagesSubcategoryHelp2": "A tabela é hierarquicamente organizada, as URLs são exibidas como uma estrutura de pastas.", + "PagesSubcategoryHelp3": "Use os ícones de mais e menos à esquerda para navegar.", + "PluginDescription": "Relatórios sobre visualizações de páginas e títulos de página. Permite que você mensure o sistema de busca interno do seu website. Registra automaticamente cliques em links externos e baixa de arquivos.", + "RevisitedPages": "Páginas visualizadas mais de uma vez", + "SearchesSegmentHelp": "Para selecionar todas as visitas que utilizaram a Pesquisa interna do Site, use: %s", + "SiteSearchCategories": "Categorias (Pesquisa no Site)", + "SiteSearchCategories1": "Este relatório lista as categorias que os visitantes selecionaram, quando eles fizeram uma pesquisa em seu site.", + "SiteSearchCategories2": "Por exemplo: os sites de comércio eletrônico têm tipicamente uma \"Categoria\" seletora de modo que os visitantes podem restringir suas pesquisas a todos os produtos de uma categoria específica.", + "SiteSearchCategory": "Categoria (Pesquisa no Site)", + "SiteSearchFollowingPagesDoc": "Quando os visitantes pesquisam em seu site, eles estão à procura de uma determinada página, conteúdo, produto ou serviço. Este relatório lista as páginas que foram mais clicadas depois de uma procura interna. Em outras palavras, a lista de páginas mais procuradas pelos visitantes já no seu site.", + "SiteSearchIntro": "Acompanhamento de pesquisas que os visitantes fazem em seu site é uma forma muito eficaz de aprender mais sobre o que o seu público está procurando, ele pode ajudar a encontrar idéias de novos conteúdos, novos produtos de comércio eletrônico que potenciais clientes podem estar procurando, e em geral, melhorar as visitas ' experiência em seu site.", + "SiteSearchKeyword": "Palavra-chave (pesquisa no site)", + "SiteSearchKeywordCount": "Contagem de palavra-chave (Procurar Site)", + "SiteSearchKeywordCounts": "Contagem de palavras-chave (Procurar Site)", + "SiteSearchKeywords": "Palavras-chave (Pesquisa no site)", + "SiteSearchKeywordsDocumentation": "Este relatório lista as Pesquisas de Palavras-chave que os visitantes procuraram em seu Motor de Busca interno.", + "SiteSearchKeywordsNoResultDocumentation": "Este relatório lista as Palavras Chave de Pesquisa que não retornaram nenhum resultado de pesquisa: talvez o algoritmo do motor de busca possa ser melhorado, ou talvez seus visitantes estão procurando por conteúdo que não estão (ainda) em seu site?", + "SiteSearchSubcategoryHelp1": "A seção Pesquisa de Sites mostra quais palavras-chave os visitantes usam ao pesquisar em seu site. Ele também exibe quais páginas os usuários visualizam após realizar uma pesquisa e quais palavras-chave de pesquisa no site não retornam nenhum resultado.", + "SiteSearchSubcategoryHelp2": "Esses relatórios podem fornecer ideias sobre a falta de conteúdo em seu site, informações sobre o que seus visitantes estão procurando, mas não conseguem encontrar facilmente e muito mais.", + "SiteSearchSubcategoryHelp3": "Saiba mais no guia de Pesquisa de Sites.", + "SubmenuPageTitles": "Títulos das páginas", + "SubmenuPagesEntry": "Páginas de entrada", + "SubmenuPagesExit": "Páginas de saída", + "SubmenuSitesearch": "Busca no Site", + "TimeSpentInReferringAction": "Tempo Gasto em Ação de Referência", + "WidgetEntryPageTitles": "Títulos da página de entrada", + "WidgetExitPageTitles": "Títulos da página de saídas", + "WidgetPageTitles": "Títulos de página", + "WidgetPageTitlesFollowingSearch": "Títulos da página seguindo uma pesquisa no site", + "WidgetPageUrlsFollowingSearch": "Seguintes páginas contêm uma pesquisa no site", + "WidgetPagesEntry": "Páginas de Entrada", + "WidgetPagesExit": "Páginas de saída", + "WidgetSearchCategories": "Categorias de busca", + "WidgetSearchKeywords": "Busca local por Palavras-Chave", + "WidgetSearchNoResultKeywords": "Palavras-Chave de Pesquisas Sem Resultados" + } +} diff --git a/plugins/Actions/lang/pt.json b/plugins/Actions/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..0474496fc3f6442f497897595f55eb6fea3ff249 --- /dev/null +++ b/plugins/Actions/lang/pt.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Tipo de ação", + "ActionTypeSegmentHelp": "Um tipo de ação, como: %s", + "ActionTypes": "Tipos de ação", + "ActionsInVisit": "Ações na visita", + "AvgGenerationTimeTooltip": "Média com base em %1$s acesso(s) %2$sentre %3$s e %4$s", + "Behaviour": "Comportamento", + "ColumnActionURL": "Endereço da ação", + "ColumnClickedURL": "Hiperligação de Saída Clicada", + "ColumnClickedURLs": "Ligações de saída clicadas", + "ColumnClicks": "Cliques", + "ColumnClicksDocumentation": "O número de vezes que esta hiperligação foi clicada.", + "ColumnDownloadURL": "URL de Descargas", + "ColumnDownloadURLs": "URLs de Descargas", + "ColumnEntryPageTitle": "Título da Página de Entrada", + "ColumnEntryPageURL": "URL da Página de Entrada", + "ColumnEntryPageURLs": "Endereços das páginas de entrada", + "ColumnExitPageTitle": "Título da Página de Saída", + "ColumnExitPageURL": "URL da Página de Saída", + "ColumnExitPageURLs": "Endereços das páginas de saída", + "ColumnIdPageview": "ID da visualização de página", + "ColumnInteractionPosition": "Posição da interação", + "ColumnNoResultKeyword": "Palavra-chave sem Resultados de Pesquisa", + "ColumnPageName": "Nome da Página", + "ColumnPageURL": "URL da Página", + "ColumnPageViewPosition": "Posição da Visualização de Página", + "ColumnPagesPerSearch": "Páginas dos Resultados de Pesquisa", + "ColumnPagesPerSearchDocumentation": "Os visitantes irão pesquisar no seu site e por vezes clicam em \"seguinte\" para ver mais resultados. Este é o número médio de páginas de resultados de pesquisa visualizados para esta palavra-chave.", + "ColumnSearchCategory": "Categoria de Pesquisa", + "ColumnSearchExits": "%% saídas de pesquisa", + "ColumnSearchExitsDocumentation": "A percentagem das visitas que saíram do site depois de pesquisarem por esta palavra-chave no seu motor de pesquisa do site.", + "ColumnSearchResultsCount": "Contagem dos Resultados da Pesquisa", + "ColumnSearches": "Pesquisas", + "ColumnSearchesDocumentation": "O número de visitas que pesquisaram por esta palavra-chave no motor de pesquisa do seu site.", + "ColumnSiteSearchKeywords": "Palavras-chave Únicas", + "ColumnUniqueClicks": "Cliques Únicos", + "ColumnUniqueClicksDocumentation": "O número de visitas que envolveu um clique nesta ligação. Se uma ligação foi clicada várias vezes durante uma visita, só é contabilizada uma vez.", + "ColumnUniqueDownloads": "Descargas Únicas", + "ColumnUniqueOutlinks": "Hiperligações de Saída Únicas", + "DownloadsReportDocumentation": "Neste relatório, pode ver quais ficheiros os visitantes descarregaram.%s O que Matomo conta como uma descarga é o clique numa hiperligação de descarga. Matomo não tem conhecimento se a descarga foi concluída ou não.", + "DownloadsSubcategoryHelp1": "Neste relatório pode ver quais ficheiros os seus visitantes descarregaram.", + "DownloadsSubcategoryHelp2": "O que o Matomo contabiliza como uma descarga é a ativação de uma ligação de descarga. O Matomo não é informado se a descarga foi concluída ou não.", + "EntryPageTitles": "Títulos das páginas de entrada", + "EntryPageTitlesReportDocumentation": "Este relatório contém informações sobre os títulos das páginas de entrada que foram utilizados durante o período especificado.", + "EntryPagesReportDocumentation": "Este relatório contém informação sobre as páginas de entrada que foram utilizadas durante o período especificado, Uma página de entrada é a primeira página que um utilizador vê durante a sua visita. %s Os endereços de entrada são apresentados numa estrutura de pastas.", + "EntryPagesSubcategoryHelp1": "Este relatório contém informações sobre as páginas de entrada que foram utilizadas durante o período especificado. Uma página de entrada é a primeira página que um utilizador visualiza durante a sua visita.", + "EntryPagesSubcategoryHelp2": "Os endereços de entrada são apresentados como uma estrutura de pastas.", + "ExitPageTitles": "Títulos das páginas de saída", + "ExitPageTitlesReportDocumentation": "Este relatório contém informações sobre os títulos de páginas de saída que ocorreram durante o período especificado.", + "ExitPagesReportDocumentation": "Este relatório contém informação sobre as páginas de saída que foram utilizadas durante o período especificado, Uma página de saída é a última página que um utilizador vê durante a sua visita. %s Os endereços de saída são apresentados numa estrutura de pastas.", + "ExitPagesSubcategoryHelp1": "Este relatório contém informações sobre as páginas de saída que utilizadas durante o período especificado. Uma página de saída é a última página que um utilizador visualiza durante a sua visita.", + "ExitPagesSubcategoryHelp2": "Os endereços de saída são apresentados como uma estrutura de pastas.", + "MainMetricsReportDocumentation": "Este relatório fornece uma visão geral muito básica das ações que os seus visitantes executaram no seu site.", + "OneSearch": "1 pesquisa", + "OutlinkDocumentation": "Uma ligação de saída é uma ligação que encaminha um visitante para fora do seu site (para outro domínio).", + "OutlinksReportDocumentation": "Este relatório mostra uma lista hierárquica de ligações de saída que foram clicadas pelos seus visitantes.", + "OutlinksSubcategoryHelp1": "Este relatório mostra uma lista hierárquica de endereços de saída que foram ativados pelos seus visitantes. Uma ligação de saída é uma ligação que afasta o visitante do seu site (para outro domínio).", + "PageTitlesReportDocumentation": "Este relatório contém informação sobre os títulos das páginas que foram visitadas. %1$s O título de página é a tag HTML %2$s que a maioria dos navegadores mostra como título respetiva janela.", + "PageTitlesSubcategoryHelp1": "Este relatório contém informações sobre os títulos das páginas que foram visitadas.", + "PageTitlesSubcategoryHelp2": "O título da página é a tag HTML &lt;title&gt; que a maioria dos navegadores utiliza para mostrar no título da respetiva janela.", + "PageUrls": "Endereços de páginas", + "PageViewsByVisitor": "O número de vezes que esta página foi vista por este visitante em todas as visitas", + "PagesReportDocumentation": "Este relatório contém informações sobre os endereços das páginas que foram visitadas. %s A tabela está organizada hierarquicamente, os endereços são apresentados como uma estrutura de pastas.", + "PagesSubcategoryHelp1": "Este relatório contém informações sobre os endereços das páginas que foram visitadas.", + "PagesSubcategoryHelp2": "A tabela é organizada hierarquicamente, os endereços são apresentados como uma estrutura de pastas.", + "PagesSubcategoryHelp3": "Utilize os ícones mais e menos à esquerda para navegar.", + "PluginDescription": "Relatórios sobre as visualizações e títulos das páginas. Permite-lhe avaliar o motor de pesquisa interno do seu site. Monitoriza automaticamente os cliques nas ligações externas e descargas de ficheiros.", + "RevisitedPages": "Páginas vistas mais do que uma vez", + "SearchesSegmentHelp": "Para selecionar todas as visitas que utilizaram a Pesquisa interna do Site, use: %s", + "SiteSearchCategories": "Categorias (pesquisa do site)", + "SiteSearchCategories1": "Este relatório lista as Categorias que os visitantes selecionaram quando realizaram uma Pesquisa no seu site.", + "SiteSearchCategories2": "Por exemplo, os sites de comércio eletrónico têm tipicamente um seletor de \"Categoria\" de modo a que os visitantes possam restringir as suas pesquisas a todos os produtos de uma categoria específica.", + "SiteSearchCategory": "Categoria (pesquisa do site)", + "SiteSearchFollowingPagesDoc": "Quando os visitantes pesquisam no seu site, eles estão à procura de uma página, conteúdo, produto ou serviço em particular. Este relatório lista as páginas que foram mais clicadas depois de uma pesquisa interna. Noutras palavras, a lista de páginas mais pesquisadas pelos visitantes dentro do seu site.", + "SiteSearchIntro": "O acompanhamento das pesquisas que os visitantes fazem no seu site é uma maneira muito eficaz de saber mais sobre o que o seu público está à procura, pode ajudar a encontrar ideias para novos conteúdos, novos produtos de comércio eletrónico que os potenciais clientes podem estar à procura e, em geral, melhorar a experiência do seu site para os visitantes.", + "SiteSearchKeyword": "Palavra-chave (Pesquisa do Site)", + "SiteSearchKeywordCount": "Contagem de palavra-chave (Pesquisa de Site)", + "SiteSearchKeywordCounts": "Contagens de palavra-chave (Pesquisa de Site)", + "SiteSearchKeywords": "Palavras-chave (pesquisa do site)", + "SiteSearchKeywordsDocumentation": "Este relatório lista as «Palavras-chave de Pesquisa» que os visitantes procuraram no seu «Motor de Pesquisa» interno.", + "SiteSearchKeywordsNoResultDocumentation": "Este relatório lista as Palavras-chave de pesquisa que não devolveram qualquer resultado de Pesquisa: talvez o algoritmo do motor de pesquisa possa ser melhorado, ou talvez os seus visitantes estejam à procura de conteúdo que (ainda) não existe no seu site?", + "SiteSearchSubcategoryHelp1": "A secção Pesquisa do site mostra quais as palavras-chave que os visitantes utilizaram para pesquisar o seu site. Também mostra que páginas os utilizadores visualizaram depois de realizar uma pesquisa e que termos de pesquisa do site não devolveram qualquer resultado.", + "SiteSearchSubcategoryHelp2": "Estes relatórios podem dar-lhe ideias sobre o conteúdo em falta no seu site, informações sobre o que os seus visitantes procuram mas não conseguem encontrar facilmente e muito mais.", + "SiteSearchSubcategoryHelp3": "Saiba mais no guia de Pesquisa do site.", + "SubmenuPageTitles": "Títulos das páginas", + "SubmenuPagesEntry": "Páginas de entrada", + "SubmenuPagesExit": "Páginas de saída", + "SubmenuSitesearch": "Pesquisa do Site", + "TimeSpentInReferringAction": "Tempo despendido na Ação de referência", + "WidgetEntryPageTitles": "Títulos de páginas de entrada", + "WidgetExitPageTitles": "Títulos de páginas de saída", + "WidgetPageTitles": "Títulos de páginas", + "WidgetPageTitlesFollowingSearch": "Títulos de páginas após uma pesquisa no site", + "WidgetPageUrlsFollowingSearch": "Páginas após uma pesquisa no site", + "WidgetPagesEntry": "Páginas de entrada", + "WidgetPagesExit": "Páginas de saída", + "WidgetSearchCategories": "Categorias de pesquisa", + "WidgetSearchKeywords": "Palavras-chave de pesquisa do site", + "WidgetSearchNoResultKeywords": "Palavras-chave de pesquisa sem resultados" + } +} diff --git a/plugins/Actions/lang/ro.json b/plugins/Actions/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..b6c1cac2c563d6eaeab1d6bb6e0f6d1d6349f66a --- /dev/null +++ b/plugins/Actions/lang/ro.json @@ -0,0 +1,65 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "Media bazată pe %1$s hit(uri) %2$s între %3$s și %4$s", + "Behaviour": "Comportament", + "ColumnActionURL": "Acţiuni URL", + "ColumnClicks": "Clicuri", + "ColumnClicksDocumentation": "Numărul de ori link-ul a fost click-uit.", + "ColumnDownloadURL": "URL Descărcare", + "ColumnEntryPageTitle": "Titlul Paginii de Intrare", + "ColumnEntryPageURL": "URL Paginii de Intrare", + "ColumnExitPageTitle": "Titlul Paginii de Ieşire", + "ColumnExitPageURL": "URL-ul Paginii de Ieşire", + "ColumnNoResultKeyword": "Cuvânt cheie fără rezultate în căutări", + "ColumnPageName": "Numele Paginii", + "ColumnPageURL": "URL-ul Paginii", + "ColumnPagesPerSearch": "Pagini cu rezultate căutări", + "ColumnPagesPerSearchDocumentation": "Vizitatorii vor căuta pe site-ul dvs web şi uneori vor apăsa \"următorul\" pentru a vizualiza mai multe rezultate. Acesta este numărul mediu de pagini cu rezultate ale căutării vizualizate pentru acest cuvânt cheie.", + "ColumnSearchCategory": "Categorie căutare", + "ColumnSearchExitsDocumentation": "Procentul vizitelor care au parasit siteul dupa ce au cautat acest Cuvant prin intermediul casutei de Search (Cautare) prezenta pe siteul tau.", + "ColumnSearchResultsCount": "Numărul rezultate căutare", + "ColumnSearches": "Căutări", + "ColumnSearchesDocumentation": "Numărul de vizite care au căutat după acest cuvânt cheie folosind motorul de căutare de pe site-ul dvs web.", + "ColumnSiteSearchKeywords": "Cuvinte-cheie unice", + "ColumnUniqueClicks": "Clicuri unice", + "ColumnUniqueClicksDocumentation": "Numărul de vizite ce a presupus un click pe acest link. Dacă un link a fost click-uit de mai multe ori în timpul unei vizite, este numărat o dată.", + "ColumnUniqueDownloads": "Descărcări Unice", + "ColumnUniqueOutlinks": "Outlink-uri Unice", + "DownloadsReportDocumentation": "În acest raport, poţi vedea ce fişiere au fost descărcate de vizitatorii tăi. %s Ceea ce Matomo socoteşte ca fiind o descărcare este un click pe un link de descărcare. Dacă a fost terminată sau nu nu este cunoscut de către Matomo.", + "EntryPageTitles": "Titlurile paginilor de intrare", + "EntryPageTitlesReportDocumentation": "Acest raport conţine informaţii despre titlurile paginilor de intrare ce au fost folosite în timpul perioadei specificate.", + "ExitPageTitles": "Titlurile paginilor de ieşire", + "ExitPageTitlesReportDocumentation": "Acest raport conţine informaţii despre titlurile paginilor de ieşire ce au fost folosite în timpul perioadei specificate.", + "OneSearch": "o căutare", + "OutlinkDocumentation": "Un outlink este un link ce trimite vizitatori de pe website-ul tău (către alt domeniu).", + "OutlinksReportDocumentation": "Acest raport arată o listă ierarhică al URL-urilor outlink-urilor ce au fost click-uite de vizitatorii tăi.", + "PageTitlesReportDocumentation": "Acest raport conţine informaţii despre titlurile paginilor ce au fost vizitate. %1$s Titlul paginii este Tag-ul HTML %2$s afişat de cele mai multe browsere ca nume al ferestrei.", + "PageUrls": "URL-uri pagini", + "PagesReportDocumentation": "Acest raport conţine informaţii despre URL-urile paginilor ce au fost vizitate. %s Acest tabel este organizat ierarhic,URL-urile sunt afişate într-o structură tip arbore.", + "PagesSubcategoryHelp3": "Folosiți semnele de plus și minus din stânga pentru navigare.", + "SiteSearchCategories": "Categorii (căutare pe site)", + "SiteSearchCategories1": "Acest raport afiseaza Categoriile pe care vizitatorii le-au selectat cand au efectuat o Cautare pe siteul tau.", + "SiteSearchCategories2": "De exemplu, magazinele online de obicei au o optiune de a selecta o anumita \"Categorie\" dintr-o lista, pentru ca vizitatorii sa restrictioneze cautarile pe site doar la produsele aflate intr-o anumita Categorie.", + "SiteSearchCategory": "Categorie (căutare pe site)", + "SiteSearchFollowingPagesDoc": "Cand vizitatorii cauta pe siteul tau, ei doresc sa gaseasca o anumita pagina, continut, produs sau serviciu. Acest raport afiseaza paginile care au fost cel mai accesate prin click dupa efectuarea unei cautari interne. Cu alte cuvinte, o lista cu paginile cele mai cautate de vizitatorii aflati deja pe siteul tau.", + "SiteSearchIntro": "A urmari cautarile pe care vizitatorii le fac pe siteul tau este o modalitate foarte eficienta de a invata ce anume cauta audienta ta, te poate ajuta sa gasesti idei pentru continut nou, noi produse pentru magazinul online pe care potentialii clienti le-ar putea cauta si, in general, pentru a imbunatati experienta utilizatorilor pe siteul tau.", + "SiteSearchKeyword": "Cuvânt-cheie (căutare pe site)", + "SiteSearchKeywords": "Cuvinte-cheie (căutare pe site)", + "SiteSearchKeywordsDocumentation": "Acest raport afiseaza Cuvintele Cautate pe care vizitatorii le-au cautat prin intermediul optiunii de Cautare prezente pe siteul tau.", + "SiteSearchKeywordsNoResultDocumentation": "Acest raport afiseaza Cuvintele Cautate care nu au returnat nici un rezultat: poate algoritmul functiei de cautare poate fi imbunatatit sau poate vizitatorii cauta continut care nu este (inca) prezent pe siteul tau?", + "SubmenuPageTitles": "Titluri pagini", + "SubmenuPagesEntry": "Pagini de intrare", + "SubmenuPagesExit": "Pagini de ieşire", + "SubmenuSitesearch": "Căutare pe site", + "WidgetEntryPageTitles": "Titlurile paginii de intrare", + "WidgetExitPageTitles": "Titlurile paginilor de ieşire", + "WidgetPageTitles": "Titluri pagini", + "WidgetPageTitlesFollowingSearch": "Titluri pagini urmărind căutarea pe site", + "WidgetPageUrlsFollowingSearch": "Pagini care urmeaza dupa o Cautare pe site.", + "WidgetPagesEntry": "Pagini de intrare", + "WidgetPagesExit": "Pagini de ieşire", + "WidgetSearchCategories": "Categorii de cautare", + "WidgetSearchKeywords": "Cuvinte-cheie pentru căutare pe site", + "WidgetSearchNoResultKeywords": "Cuvinte-cheie de căutare fără rezultat" + } +} diff --git a/plugins/Actions/lang/ru.json b/plugins/Actions/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..137b9b6525e471bd9ccfe2f96fa05d8b2523be55 --- /dev/null +++ b/plugins/Actions/lang/ru.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Тип действия", + "ActionTypes": "Типы действия", + "ActionsInVisit": "Действия за посещение", + "AvgGenerationTimeTooltip": "В среднем по %1$s хит(ов) %2$s между %3$s и %4$s", + "Behaviour": "Содержание", + "ColumnActionURL": "URL действия", + "ColumnClickedURL": "Нажатые исходящие ссылки", + "ColumnClickedURLs": "Нажатые исходящие ссылки", + "ColumnClicks": "Клики", + "ColumnClicksDocumentation": "Количество кликов по этой ссылке.", + "ColumnDownloadURL": "Ссылка для загрузки", + "ColumnDownloadURLs": "Ссылки для загрузки", + "ColumnEntryPageTitle": "Заголовок страницы входа", + "ColumnEntryPageURL": "URL страницы входа", + "ColumnEntryPageURLs": "URLы страниц входа", + "ColumnExitPageTitle": "Заголовок страницы выхода", + "ColumnExitPageURL": "URL страницы выхода", + "ColumnExitPageURLs": "URLы страниц выхода", + "ColumnIdPageview": "ID просмотра", + "ColumnInteractionPosition": "Местоположение взаимодействия", + "ColumnNoResultKeyword": "Ключевое слово без результатов в поиске", + "ColumnPageName": "Название страницы", + "ColumnPageURL": "URL страницы", + "ColumnPageViewPosition": "Позиция просмотра страницы", + "ColumnPagesPerSearch": "Страницы результатов поиска", + "ColumnPagesPerSearchDocumentation": "Посетители будут пользоваться поиском на вашем сайте и иногда нажимать \"next\" (след. страница), чтобы просмотреть больше результатов. Это среднее число страниц с результатами поиска для этого ключевого слова.", + "ColumnSearchCategory": "Искать категорию", + "ColumnSearchExits": "%% уходов с поиска", + "ColumnSearchExitsDocumentation": "Процент посещений, когда посетитель ушёл с сайта после поиска на вашем сайте именно этого ключевого слова.", + "ColumnSearchResultsCount": "Счётчик результатов поиска", + "ColumnSearches": "Поиски", + "ColumnSearchesDocumentation": "Число посещений, когда посетитель искал именно это ключевое слово через поиск на сайте.", + "ColumnSiteSearchKeywords": "Уникальные ключевые слова", + "ColumnUniqueClicks": "Уникальные клики", + "ColumnUniqueClicksDocumentation": "Количество посещений, когда была кликнута эта ссылка. Если ссылка была кликнута несколько раз в течение одного и того же посещения, это учитывается лишь раз.", + "ColumnUniqueDownloads": "Уникальные загрузки", + "ColumnUniqueOutlinks": "Уникальные исходящие ссылки", + "DownloadsReportDocumentation": "В этом отчёте вы можете видеть файлы, которые скачивали ваши посетители. %s За скачивание считается клик по соответствующей ссылке. Однако закончил ли посетитель скачивание остается неизвестным. За скачивание считается также открытие картинок и других расширений файлов.", + "DownloadsSubcategoryHelp1": "В этом отчете вы можете увидеть, какие файлы скачали ваши посетители.", + "DownloadsSubcategoryHelp2": "Что Matomo считает загрузкой, так это щелчок по ссылке для загрузки. Была ли загрузка завершена, Matomo не известно.", + "EntryPageTitles": "Заголовки страниц входа", + "EntryPageTitlesReportDocumentation": "Этот отчёт содержит информацию о заголовках входных страниц, на которые попадали посетители в конкретный период.", + "EntryPagesReportDocumentation": "Этот отчет содержит информацию о начальных страницах за определенный период. Начальная страница – это первая страница сайта, которую пользователь видит во время своего посещения. %s Начальные ссылки отображаются в виде структуры, состоящей из папок.", + "EntryPagesSubcategoryHelp1": "Этот отчет содержит информацию о страницах входа, которые использовались в течение указанного периода. Страница входа — это первая страница, которую просматривает пользователь во время посещения.", + "EntryPagesSubcategoryHelp2": "URL-адреса входов отображаются в виде структуры папок.", + "ExitPageTitles": "Заголовки страниц выхода", + "ExitPageTitlesReportDocumentation": "Этот отчёт содержит информацию о заголовках выходных страниц, с которых посетитель ушел с сайта в конкретный период.", + "ExitPagesReportDocumentation": "Этот отчет содержит информацию о страницах выхода за определенный период. Страница выхода – это последняя страница сайта, которую пользователь видит во время своего посещения. %s Ссылки выхода отображаются в виде структуры, состоящей из папок.", + "ExitPagesSubcategoryHelp1": "Этот отчет содержит информацию о страницах выхода, произошедших за указанный период. Страница выхода — это последняя страница, которую просматривает пользователь во время посещения.", + "ExitPagesSubcategoryHelp2": "URL-адреса выходов отображаются в виде структуры папок.", + "MainMetricsReportDocumentation": "Этот отчет предоставляет очень общий обзор того, какие действия ваши посетители совершают на вашем веб-сайте.", + "OneSearch": "1 поиск", + "OutlinkDocumentation": "Исходящая ссылка — ссылка, которая уводит посетителя с вашего сайта (на другой домен).", + "OutlinksReportDocumentation": "Этот отчёт показывает иерархический список исходящих ссылок, на которые кликнул пользователь.", + "OutlinksSubcategoryHelp1": "В этом отчете показан иерархический список URL-адресов исходящих ссылок, по которым переходили ваши посетители. Исходящая ссылка — это ссылка, которая уводит посетителя с вашего сайта (на другой домен).", + "PageTitlesReportDocumentation": "Этот отчёт содержит информацию о заголовках посещенных страниц. %1$s Заголовок страницы это HTML-тег %2$s, который браузеры отображают в заголовке окна (вкладки).", + "PageTitlesSubcategoryHelp1": "Этот отчет содержит информацию о заголовках посещенных страниц.", + "PageTitlesSubcategoryHelp2": "Заголовок страницы — это тег HTML &lt;title&gt; Тег, который большинство браузеров отображают в заголовке окна.", + "PageUrls": "URL-ы страниц", + "PageViewsByVisitor": "Количество просмотров страницы этой страницы данного посетителя во всех посещениях", + "PagesReportDocumentation": "Этот отчёт содержит информацию об адресах страниц, на которых побывали посетители вашего сайта. %s Таблица иерархично организована — URL-ы отображаются структурой папок.", + "PagesSubcategoryHelp1": "Этот отчет содержит информацию об URL-адресах посещенных страниц.", + "PagesSubcategoryHelp2": "Таблица организована иерархически, URL-адреса отображаются в виде структуры папок.", + "PagesSubcategoryHelp3": "Используйте иконки плюс и минус слева для навигации.", + "PluginDescription": "Отчёты о просмотрах и заголовках страниц. Позволяют оценить внутренний поисковый движок сайта. Автоматически отслеживает клики по внешним ссылкам и скачиванию файлов.", + "RevisitedPages": "Просмотры страниц более одного раза", + "SiteSearchCategories": "Категории (поиск по сайту)", + "SiteSearchCategories1": "Этот отчёт показывает список категорий, которые выбрали посетители при поиске на вашем сайте.", + "SiteSearchCategories2": "Например, сайты электронной коммерции обычно имеют выбор «Категорий», чтобы посетителям было удобно их сортировать и искать.", + "SiteSearchCategory": "Категория (поиск по сайту)", + "SiteSearchFollowingPagesDoc": "Когда посетители ищут что-то на вашем сайте, они хотят найти определенную страницу, контент, продукт или услугу. Этот отчёт отображает страницы, по которым посетители кликают наиболее часто при поиске. Иными словами — это список страниц, которые ищутся наиболее часто на вашем сайте.", + "SiteSearchIntro": "Отслеживание поиска на вашем сайте — эффективный способ узнать, чем именно интересуется ваша аудитория. Это может помочь найти идеи для нового контента, новых продуктов и улучшить взаимодействие посетителей с вашим сайта в целом.", + "SiteSearchKeyword": "Ключевое слово (поиск по сайту)", + "SiteSearchKeywordCount": "Количество ключевых слов (поиск по сайту)", + "SiteSearchKeywordCounts": "Количество ключевых слов (поиск по сайту)", + "SiteSearchKeywords": "Ключевые слова (поиск по сайту)", + "SiteSearchKeywordsDocumentation": "Этот отчёт показывает список ключевых слов, которые посетители искали на вашем сайте через ваш поиск.", + "SiteSearchKeywordsNoResultDocumentation": "Этот отчёт отображает список ключевых слов, по которым не было ничего найдено на вашем сайте: возможно, стоит улучшить поисковый алгоритм на вашем сайте или посетители ищут то, чего на вашем сайте (пока) нет?", + "SiteSearchSubcategoryHelp1": "Раздел \"Поиск по сайту\" показывает, какие ключевые слова используют посетители при поиске на вашем сайте. Он также показывает, какие страницы пользователи просматривают после выполнения поиска и какие ключевые слова поиска на сайте вообще не дают результатов.", + "SiteSearchSubcategoryHelp2": "Эти отчеты могут дать вам представление об отсутствующем контенте на вашем сайте, понять, что ваши посетители ищут, но не могут легко найти, и многое другое.", + "SiteSearchSubcategoryHelp3": "Узнайте больше в руководстве по поиску по сайту.", + "SubmenuPageTitles": "Заголовки страниц", + "SubmenuPagesEntry": "Страницы входа", + "SubmenuPagesExit": "Страницы выхода", + "SubmenuSitesearch": "Поиск по сайту", + "TimeSpentInReferringAction": "Время, потраченное на реферальное действие", + "WidgetEntryPageTitles": "Заголовки страниц входа", + "WidgetExitPageTitles": "Заголовки страницы выхода", + "WidgetPageTitles": "Заголовки страниц", + "WidgetPageTitlesFollowingSearch": "Заголовки страниц, которые нашлись в поиске", + "WidgetPageUrlsFollowingSearch": "Страницы, которые нашлись в поиске", + "WidgetPagesEntry": "Страницы входа", + "WidgetPagesExit": "Страницы выхода", + "WidgetSearchCategories": "Категории поиска", + "WidgetSearchKeywords": "Поисковые ключевые слова сайта", + "WidgetSearchNoResultKeywords": "Поисковые ключевые слова без результатов" + } +} diff --git a/plugins/Actions/lang/si.json b/plugins/Actions/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Actions/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Actions/lang/sk.json b/plugins/Actions/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..acee00b2e713156569da1d4b2f50ceb646b003d1 --- /dev/null +++ b/plugins/Actions/lang/sk.json @@ -0,0 +1,40 @@ +{ + "Actions": { + "ColumnClicks": "Kliknutia", + "ColumnClicksDocumentation": "Počet kliknutí na odkaz.", + "ColumnDownloadURL": "Stiahnuté URL", + "ColumnEntryPageTitle": "Titulok vstupnej stránky", + "ColumnEntryPageURL": "URL vstupnej stránky", + "ColumnExitPageTitle": "Titulok výstupnej stránky", + "ColumnExitPageURL": "URL výstupnej stránky", + "ColumnNoResultKeyword": "Kľúčové slovo bez výsledku vyhľadávania", + "ColumnPageName": "Názov stránky", + "ColumnPageURL": "Adresa stránky", + "ColumnPagesPerSearch": "Stránky s výsledkami vyhľadávania", + "ColumnPagesPerSearchDocumentation": "Návštevníci budú vyhľadávať na Vašich webových stránkach a niekedy klikniknú na \"ďalšie\" pre zobrazenie viac výsledkov. Toto je priemerný počet stránok s výsledkami vyhľadávania zobrazených pre toto kľúčové slovo.", + "ColumnSearchCategory": "Kategória vyhľadávania", + "ColumnSearchResultsCount": "Počet výsledkov vyhľadávania", + "ColumnSearches": "Vyhľadávania", + "ColumnSiteSearchKeywords": "Unikátne kľúčové slová", + "ColumnUniqueClicks": "Jedinečné kliknutia", + "ColumnUniqueDownloads": "Jedinečné sťahovania", + "ColumnUniqueOutlinks": "Unikátne vonkajšie odkazy", + "EntryPageTitles": "Titulky vstupnej stránky", + "ExitPageTitles": "Titulky výstupnej stránky", + "PageUrls": "URL stránok", + "PagesSubcategoryHelp3": "Pre navigáciu použite symboly plus a mínus vľavo.", + "SiteSearchKeyword": "Kľúčové slovo (vyhľadávanie na stránke)", + "SubmenuPageTitles": "Názov stránky", + "SubmenuPagesEntry": "Vstupné stránky", + "SubmenuPagesExit": "Výstupné stránky", + "WidgetEntryPageTitles": "Titulky vstupných stránok", + "WidgetExitPageTitles": "Titulky výstupných stránok", + "WidgetPageTitles": "Titulky stránok", + "WidgetPageTitlesFollowingSearch": "Titulky stránok po vyhľadávaní na stránkach", + "WidgetPageUrlsFollowingSearch": "Stránky po vyhľadávaní na stránkach", + "WidgetPagesEntry": "Vstupné stránky", + "WidgetPagesExit": "Výstupné stránky", + "WidgetSearchCategories": "Kategórie hľadania", + "WidgetSearchKeywords": "Kľúčové slová vyhladávania" + } +} diff --git a/plugins/Actions/lang/sl.json b/plugins/Actions/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..2ac09b1b511200e4a809ce80966453603a003213 --- /dev/null +++ b/plugins/Actions/lang/sl.json @@ -0,0 +1,48 @@ +{ + "Actions": { + "ColumnClicks": "Kliki", + "ColumnClicksDocumentation": "Število klikov na to povezavo.", + "ColumnDownloadURL": "URL za prenos", + "ColumnEntryPageTitle": "Naslov vstopne strani", + "ColumnEntryPageURL": "URL vstopne strani", + "ColumnExitPageTitle": "Naslov izhodne strani", + "ColumnExitPageURL": "URL izhodne strani", + "ColumnNoResultKeyword": "Ključna beseda brez zadetka", + "ColumnPageName": "Ime spletne strani", + "ColumnPageURL": "URL spletne strani", + "ColumnSearchCategory": "Kategorija iskanja", + "ColumnSearchResultsCount": "Število zadetkov pri iskanju", + "ColumnSearches": "Iskanja", + "ColumnSiteSearchKeywords": "Unikatnih ključnih besed", + "ColumnUniqueClicks": "Edinstveni kliki", + "ColumnUniqueClicksDocumentation": "Število obiskov, ki so vsebovali klik na to povezavo. Če je bilo klikov na isto povezavo več v istem obisku, se to šteje kot en sam klik.", + "ColumnUniqueDownloads": "Edinstveni prenosi", + "ColumnUniqueOutlinks": "Edinstvene izhodne povezave", + "DownloadsReportDocumentation": "V tem poročilu lahko visite, katere datoteke so prenesli vaši uporabniki. %s Matomo šteje vse klike na povezavo za prenos. Matomo ne ve ali je bila datoteka uspešno prenesena.", + "EntryPageTitles": "Nazivi vhodnih strani", + "EntryPageTitlesReportDocumentation": "To poročilo vsebuje informacije o nazivih vhodnih strani, ki so bile obiskane v izbranem časovnam obdobju.", + "ExitPageTitles": "Nazivi izhodnih strani", + "ExitPageTitlesReportDocumentation": "To poročilo vsebuje informacije o nazivih izhodnih strani, ki so bile obiskane v izbranem časovnam obdobju.", + "OneSearch": "Eno iskanje", + "OutlinkDocumentation": "Zunanja povezava je povezava, ki vodi obiskovalca stran od vaše splete strani (na drugo domeno).", + "OutlinksReportDocumentation": "To poročilo prikazuje hierarhični seznam URL-jev zunanjih povezav na katere so uporabniki kliknili.", + "PageTitlesReportDocumentation": "To poročilo vsebuje informacije o nazivih strani, ki so bile obiskane. %1$s Naziv strani je HTML oznaka %2$s Oznaka, ki jo večina brskalnikov prikaže v nazivu okna.", + "PageUrls": "URL-ji strani", + "PagesReportDocumentation": "To poročilo vsebuje informacije o URL-jih strani, ki so bile obiskane. %s Tabela je hierarhično organizirana. URL-ji so prikazani kot struktura mape.", + "PagesSubcategoryHelp3": "Za navigacijo uporabite ikoni plus in minus na levi.", + "SiteSearchKeyword": "Ključna beseda (iskanje po strani)", + "SubmenuPageTitles": "Naslovi strani", + "SubmenuPagesEntry": "Vstopne strani", + "SubmenuPagesExit": "Izhodne strani", + "SubmenuSitesearch": "Iskanje po strani", + "WidgetEntryPageTitles": "Naslovi vstopnih strani", + "WidgetExitPageTitles": "Naslovi izhodnih strani", + "WidgetPageTitles": "Naslovi strani", + "WidgetPageUrlsFollowingSearch": "Strani obiskane po iskanju na strani", + "WidgetPagesEntry": "Vstopne strani", + "WidgetPagesExit": "Izhodne strani", + "WidgetSearchCategories": "Kategorije za iskanje", + "WidgetSearchKeywords": "Ključne besede", + "WidgetSearchNoResultKeywords": "Ključne besede brez zadetkov" + } +} diff --git a/plugins/Actions/lang/sq.json b/plugins/Actions/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..71f40002ec2894dfccbe2013afb3abc044a6964b --- /dev/null +++ b/plugins/Actions/lang/sq.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Lloj Veprimi", + "ActionTypeSegmentHelp": "Një lloj veprimi, fjala vjen: %s", + "ActionTypes": "Lloje Veprimesh", + "ActionsInVisit": "Veprime Në Vizitë", + "AvgGenerationTimeTooltip": "Mesatare e bazuar në %1$s “hit”(e) %2$s mes %3$s dhe %4$s", + "Behaviour": "Sjellje", + "ColumnActionURL": "URL Veprimi", + "ColumnClickedURL": "Lidhje për Jashtë e Klikuar", + "ColumnClickedURLs": "Lidhje për Jashtë të Klikuara", + "ColumnClicks": "Klikime", + "ColumnClicksDocumentation": "Sa herë është klikuar kjo lidhje.", + "ColumnDownloadURL": "URL shkarkimesh", + "ColumnDownloadURLs": "URL Shkarkimesh", + "ColumnEntryPageTitle": "Titull Faqeje Hyrëse", + "ColumnEntryPageURL": "URL Faqeje Hyrëse", + "ColumnEntryPageURLs": "URL Faqesh Hyrëse", + "ColumnExitPageTitle": "Titull Faqeje Dalëse", + "ColumnExitPageURL": "URL Faqeje Dalëse", + "ColumnExitPageURLs": "URL Faqesh Dalëse", + "ColumnIdPageview": "ID Parjeje Faqeje", + "ColumnInteractionPosition": "Pozicion Ndërveprimi", + "ColumnNoResultKeyword": "Fjalëkyçe me “Pa Përfundime Kërkimi”", + "ColumnPageName": "Emër Faqeje", + "ColumnPageURL": "URL faqesh", + "ColumnPageViewPosition": "Pozicion Sipas Parjesh Faqeje", + "ColumnPagesPerSearch": "Faqe “Përfundime Kërkimi”", + "ColumnPagesPerSearchDocumentation": "Vizitorët do të kërkojnë te sajti juaj dhe ndonjëherë klikojnë “pasuesja”, që të shohin më tepër përfundime. Kjo është mesatarja e numrit të faqeve me përfundime kërkimi të para për këtë fjalëkyç.", + "ColumnSearchCategory": "Kategori Kërkimesh", + "ColumnSearchExits": "%% Largime Nga Kërkimi", + "ColumnSearchExitsDocumentation": "Përqindja e vizitave që përfunduan me largim nga sajti pas kërkimit për këtë Fjalëkyç te motori i Kërkimeve në Sajtin tuaj.", + "ColumnSearchResultsCount": "Numër Përfundimesh Kërkimi", + "ColumnSearches": "Kërkime", + "ColumnSearchesDocumentation": "Numri i vizitave nga të cilat pati kërkime për këtë fjalëkyç te motori i kërkimit në sajtin tuaj.", + "ColumnSiteSearchKeywords": "Fjalëkyçe Unike", + "ColumnUniqueClicks": "Klikime Unike", + "ColumnUniqueClicksDocumentation": "Numri i vizitave që kanë sjellë klikim mbi këtë lidhje. Nëse një lidhje është klikuar shumë herë gjatë një vizite, llogaritet vetëm një herë.", + "ColumnUniqueDownloads": "Shkarkime Unike", + "ColumnUniqueOutlinks": "Lidhje për Jashtë Unike", + "DownloadsReportDocumentation": "Në këtë raport, mund të shihni cilat kartela kanë shkarkuar vizitorët tuaj. %s Ato çka Matomo numëron si shkarkim është çdo klikim mbi një lidhje shkarkimi. Por Matomo s’mund ta dijë a u plotësua apo jo shkarkimi.", + "DownloadsSubcategoryHelp1": "Në këtë raport mund të shihni se cilat kartela kanë shkarkuar vizitorët tuaj.", + "DownloadsSubcategoryHelp2": "Ato çka Matomo numëron si shkarkim është çdo klikim mbi një lidhje shkarkimi. Por Matomo s’mund ta dijë a u plotësua apo jo shkarkimi.", + "EntryPageTitles": "Tituj faqesh hyrje", + "EntryPageTitlesReportDocumentation": "Ky raport përmban të dhëna rreth titujsh faqesh hyrje që janë përdorur gjatë periudhës së dhënë.", + "EntryPagesReportDocumentation": "Ky raport përmban të dhëna rreth faqesh hyrjesh që janë përdorur gjatë një periudhe të caktuar. Faqe hyrjeje është faqja e parë që sheh një vizitor gjatë vizitës së tij. %s URL-të e hyrjeve shfaqen sipas një strukture dosjeje.", + "EntryPagesSubcategoryHelp1": "Ky raport përmban të dhëna rreth faqesh hyrjesh që janë përdorur gjatë një periudhe të caktuar. Faqe hyrjeje është faqja e parë që sheh një vizitor gjatë vizitës së tij.", + "EntryPagesSubcategoryHelp2": "URL-të e hyrjeve tregohen si strukturë dosjeje.", + "ExitPageTitles": "Tituj faqesh dalje", + "ExitPageTitlesReportDocumentation": "Ky raport përmban të dhëna rreth titujve të faqeve dalje që janë përdorur gjatë periudhës së dhënë.", + "ExitPagesReportDocumentation": "Ky raport përmban të dhëna rreth faqesh daljesh që janë kryer gjatë një periudhe të caktuar. Faqe daljeje është faqja e fundit që sheh një vizitor gjatë vizitës së tij. %s URL-të e daljeve shfaqen sipas një strukture dosjeje.", + "ExitPagesSubcategoryHelp1": "Ky raport përmban të dhëna rreth faqesh daljesh që janë përdorur gjatë një periudhe të caktuar. Faqe daljeje është faqja e fundit që sheh një vizitor gjatë vizitës së tij.", + "ExitPagesSubcategoryHelp2": "URL-të e daljeve tregohen si strukturë dosjeje.", + "MainMetricsReportDocumentation": "Ky raport furnizon një përmbledhje shumë elementare rreth çfarë veprimesh bëjnë vizitorët në sajtin tuaj.", + "OneSearch": "1 kërkim", + "OutlinkDocumentation": "Një lidhje për jashtë është një lidhje që e shpie përdoruesin diku jashtë sajtit tuaj (te një tjetër përkatësi).", + "OutlinksReportDocumentation": "Ky raport shfaq një listë hierarkike URL-sh lidhjesh për jashtë që janë klikuar nga vizitorët tuaj.", + "OutlinksSubcategoryHelp1": "Ky raport shfaq një listë hierarkike URL-sh lidhjesh për jashtë që janë klikuar nga vizitorët tuaj. Një lidhje për jashtë është një lidhje që e largon vizitorin nga sajti juaj (drejt një përkatësie tjetër).", + "PageTitlesReportDocumentation": "Ky raport përmban të dhëna rreth titujve të faqeve që janë vizituar. %1$s Titulli i faqes është Etiketa HTML %2$s që shfaqin shumica e shfletuesve te titulli i dritares.", + "PageTitlesSubcategoryHelp1": "Ky raport përmban të dhëna rreth titujve të faqeve që janë vizituar.", + "PageTitlesSubcategoryHelp2": "Titulli i faqeve është Etiketa HTML &lt;title&gt; që shfaqin në titullin e dritares shumica e faqeve.", + "PageUrls": "URL faqesh", + "PageViewsByVisitor": "Numër herësh që kjo faqe qe parë nga ky vizitor gjatë krejt vizitave", + "PagesReportDocumentation": "Ky raport përmban të dhëna rreth URL faqesh që janë vizituar. %s Tabela është e sistemuar në mënyrë hierarkike, URL-të tregohen si strukturë dosjeje.", + "PagesSubcategoryHelp1": "Ky raport përmban të dhëna rreth URL-sh faqesh që janë vizituar.", + "PagesSubcategoryHelp2": "Tabela është e sistemuar në mënyrë hierarkike, URL-të tregohen si strukturë dosjeje.", + "PagesSubcategoryHelp3": "Për lëvizje përdorni ikonat plus dhe minus majtas.", + "PluginDescription": "Raporte rreth parjes së faqeve dhe titujve të tyre. Ju lejon të matni gjëra për motorin e brendshëm të kërkimeve të sajtit tuaj. Ndjek vetvetiu klikimet drejt lidhjesh të jashtme dhe shkarkime kartelash.", + "RevisitedPages": "Faqe të para më shumë se një herë", + "SearchesSegmentHelp": "Për të përzgjedhur krejt vizitat që përdorën Kërkimin e brendshëm të Sajtit, përdorni: %s", + "SiteSearchCategories": "Kategori (Kërkim Në Sajt)", + "SiteSearchCategories1": "Ky raport paraqet kategoritë që vizitorët kanë përzgjedhur kur bënë një Kërkim te sajti juaj.", + "SiteSearchCategories2": "Për shembull, sajtet për e-tregti kanë zakonisht një përzgjedhës “Kategorish”, që kështu vizitorët të mund t’i ngushtojnë kërkimet e tyre nga krejt produktet te një Kategori specifike.", + "SiteSearchCategory": "Kategori (Kërkim Në Sajt)", + "SiteSearchFollowingPagesDoc": "Kur vizitorët kërkojnë te sajti juaj, ata shohin për një faqe, lëndë, produkt ose shërbim të veçantë. Ky raport paraqet faqet që qenë klikuar më shumë pas një kërkimi të brendshëm. Me fjalë të tjera, listën e faqeve më të kërkuara nga vizitorët tashmë brenda sajtit tuaj.", + "SiteSearchIntro": "Ndjekja e kërkimeve që bëjnë vizitorët në sajtin tuaj është një rrugë shumë e efektshme për të mësuar më tepër rreth çka po kërkon publiku juaj, mund t’ju ndihmojë të gjeni ide për lëndë të re, produkte të reja e-tregtie që mund të jenë duke u kërkuar nga blerës potencialë dhe, përgjithësisht, të përmirësoni për vizitorët punimin e sajtit tuaj.", + "SiteSearchKeyword": "Fjalëkyç (Kërkim Në Sajt)", + "SiteSearchKeywordCount": "Numër fjalëkyçesh (Kërkim Në Sajt)", + "SiteSearchKeywordCounts": "Numër fjalëkyçesh (Kërkim Në Sajt)", + "SiteSearchKeywords": "Fjalëkyça (Kërkim Në Sajt)", + "SiteSearchKeywordsDocumentation": "Ky raport paraqet Fjalëkyçe Kërkimesh që vizitorët kanë bërë me Motorin tuaj të brendshëm të Kërkimeve.", + "SiteSearchKeywordsNoResultDocumentation": "Ky raport paraqet Fjalëkyçe Kërkimesh që s’patën ndonjë përfundim kërkimi: ndoshta algoritmi i motorit të kërkimit mund të përmirësohet, ose ndoshta vizitorët tuaj po kërkonin për lëndë që s’gjendet (ende) në sajtin tuaj?", + "SiteSearchSubcategoryHelp1": "Ndarja Kërkim në Sajt shfaq cilat fjalëkyçe përdorin vizitorët kur kërkojnë në sajtin tuaj. Shfaq gjithashtu cilat faqe shohin vizitorët pasi bëjnë një kërkim dhe cilat fjalëkyçe në sajt nuk japin asnjë përfundim.", + "SiteSearchSubcategoryHelp2": "Këto raporte mund t’ju japin ide rreth lënde që mungon në sajtin tuaj, se çfarë po kërkojnë vizitorët, por s’e gjejnë kollaj, etj.", + "SiteSearchSubcategoryHelp3": "Mësoni më tepër, te Udhërrëfyesi i Kërkimit Në Sajt.", + "SubmenuPageTitles": "Tituj faqesh", + "SubmenuPagesEntry": "Faqe hyrjesh", + "SubmenuPagesExit": "Faqe daljesh", + "SubmenuSitesearch": "Kërkim Në Sajt", + "TimeSpentInReferringAction": "Kohë e Harxhuar në Veprim Sjelljeje", + "WidgetEntryPageTitles": "Tituj Faqesh Hyrje", + "WidgetExitPageTitles": "Tituj Faqesh Dalje", + "WidgetPageTitles": "Tituj Faqesh", + "WidgetPageTitlesFollowingSearch": "Tituj Faqesh Në Vijim të një Kërkimi në Sajt", + "WidgetPageUrlsFollowingSearch": "Faqe Në Vijim të një Kërkimi në Sajt", + "WidgetPagesEntry": "Faqe Hyrje", + "WidgetPagesExit": "Faqe Daljesh", + "WidgetSearchCategories": "Kategori Kërkimesh", + "WidgetSearchKeywords": "Fjalëkyçe Kërkimi në Sajt", + "WidgetSearchNoResultKeywords": "Fjalëkyçe Kërkimesh me Pa Përfundime" + } +} diff --git a/plugins/Actions/lang/sr.json b/plugins/Actions/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..f0f2b1a7cd3009d864b6ae1ff85f23b0f4b026a3 --- /dev/null +++ b/plugins/Actions/lang/sr.json @@ -0,0 +1,68 @@ +{ + "Actions": { + "ActionType": "Tip akcije", + "AvgGenerationTimeTooltip": "Prosek zasnovan na %1$s pogodaka %2$s između %3$s i %4$s", + "ColumnActionURL": "URL akcije", + "ColumnClicks": "Klikovi", + "ColumnClicksDocumentation": "Broj klikova na ovaj link.", + "ColumnDownloadURL": "Download link", + "ColumnEntryPageTitle": "Naslov ulazne strane", + "ColumnEntryPageURL": "Adresa ulazne strane", + "ColumnExitPageTitle": "Naslov izlazne strane", + "ColumnExitPageURL": "Adresa izlazne strane", + "ColumnIdPageview": "ID prikaza stranice", + "ColumnInteractionPosition": "Pozicija interakcije", + "ColumnNoResultKeyword": "Ključne reči koje nisu vratile rezultate pretrage", + "ColumnPageName": "Naslov stranice", + "ColumnPageURL": "Adresa stranice", + "ColumnPagesPerSearch": "Rezultati pretrage", + "ColumnPagesPerSearchDocumentation": "Korisnici koji pretražuju vaš sajt često kliknu na \"sledeći\" kako bi videli više rezultata. Ovo je prosečan broj strana sa rezultatima pretrage za ovu ključnu reč.", + "ColumnSearchCategory": "Kategorija pretrage", + "ColumnSearchExits": "%% Pretraga postoji", + "ColumnSearchExitsDocumentation": "Procenat poseta koje su napustile sajt nakom traženja ove ključne reči.", + "ColumnSearchResultsCount": "Broj rezultata pretrage", + "ColumnSearches": "Pretrage", + "ColumnSearchesDocumentation": "Broj poseta koje su rezultirale traženjem ove ključne reči.", + "ColumnSiteSearchKeywords": "Jedinstvene ključne reči", + "ColumnUniqueClicks": "Jedinstveni klikovi", + "ColumnUniqueClicksDocumentation": "Broj poseta koje uključuju klik na ovaj link. Ako je link kliknut više puta tokom jedne posete, računa se samo jedan klik.", + "ColumnUniqueDownloads": "Jedinstvenih preuzimanja", + "ColumnUniqueOutlinks": "Jedinstveni izlazni linkovi", + "DownloadsReportDocumentation": "U ovom izveštaju možete videti koje su fajlove posetioci kliknuli. %s Ono što Matomo računa kao download jeste klik na link za skidanje fajla. Bilo da je fajl skinut ili ne, to nije poznato Matomo-u.", + "EntryPageTitles": "Naslovi ulaznih stranica", + "EntryPageTitlesReportDocumentation": "Ovaj izveštaj sadrži informacije o naslovima ulaznih stranica koje su korišćene tokom određenog perioda.", + "EntryPagesReportDocumentation": "Ovaj izveštaj sadrži informacije o ulaznim stranicama koje su korišćene tokom određenog perioda. Ulazna stranica je prva stranica koju posetilac vidi tokom svoje posete. %s Ulazni linkovi su prikazani kao stablo foldera.", + "ExitPageTitles": "Naslovi izlaznih stranica", + "ExitPageTitlesReportDocumentation": "Ovaj izveštaj sadrži informacije o naslovima izlaznih stranica koje su se desile tokom određenog perioda.", + "ExitPagesReportDocumentation": "Ovaj izveštaj sadrži informacije o izlaznim stranicama koje su se desile tokom određenog perioda. Izlazna stranica je poslednja stranica koju posetilac vidi tokom svoje posete. %s Izlazni linkovi su prikazani kao stablo foldera.", + "OneSearch": "Jedna pretraga", + "OutlinkDocumentation": "Odlazni link je link koji posetioca vodi sa Vašeg sajta (ka nekom drugom).", + "OutlinksReportDocumentation": "Ovaj izveštaj pokazuje hijerarhijsku listu odlaznih linkova koje su Vaši posetioci kliknuli.", + "PageTitlesReportDocumentation": "Ovaj izveštaj sadrži informacije o naslovima stranica koje su posećene. %1$s Naslov stranice je HTML %2$s tag koji većina pregledača prikazuje umesto svog naslova.", + "PageUrls": "Adrese stranica", + "PagesReportDocumentation": "Ovaj izveštaj sadrži informacije o linkovima koji su posećeni. %s Tabela je organizovana hijerarhijski, linkovi su prikazani u vidu foldera.", + "PagesSubcategoryHelp3": "Za navigaciju koristite sličice plus i minus s leve strane.", + "PluginDescription": "Izveštaj o prikazima i naslovima stranica. Omogućava vam da pratite vaš interni pretraživač sajta. Automatski prati klikove na eksterne linkove i preuzimanja fajlova.", + "SiteSearchCategories1": "Ovaj izveštaj prikazuje kategorije koje posetioci biraju kada pretražuju vaš sajt.", + "SiteSearchCategories2": "Na primer, sajtovi za elektronsku trgovinu obično nude mogućnost izbora kategorije kako bi posetioci ograničili svoju pretragu na proizvode iz određene kategorije.", + "SiteSearchFollowingPagesDoc": "Kada posetioci pretražuju vaš sajt, oni traže određenu stranicu, sadržaj, proizvod ili uslugu. Ovaj izveštaj prikazuje stranice koje su najčešće otvarane nakom završene pretrage. Drugim rečima, ovo je spisak stranica koje se najviše traže na vašem sajtu.", + "SiteSearchIntro": "Praćenje pretraga koje vrše vaši posetioci je efikasan način shvatanja šta vaši posetioci traže, pomaže u iznalaženju novih ideja i koncepata, novih proizvoda koje potencijalni kupci traže i generalno unapređuje korisničko iskustvo na vašem sajtu.", + "SiteSearchKeyword": "Ključna reč (Pretraživanje)", + "SiteSearchKeywordsDocumentation": "Ovaj izveštaj prikazuje ključne reči koje su vaši posetioci tražili u polju za pretragu.", + "SiteSearchKeywordsNoResultDocumentation": "Ovaj izveštaj prikazuje ključne reči koje nisu vratile nijedan rezultat pretrage. Možda algoritam pretrage može da se poboljša ili možda posetioci traže sadržaje koji (još uvek) nisu na sajtu?", + "SubmenuPageTitles": "Naslovi stranica", + "SubmenuPagesEntry": "Ulazne stranice", + "SubmenuPagesExit": "Izlazne stranice", + "SubmenuSitesearch": "Pretraživanje sajta", + "WidgetEntryPageTitles": "Naslovi ulaznih stranica", + "WidgetExitPageTitles": "Naslovi izlaznih stranica", + "WidgetPageTitles": "Naslovi stranica", + "WidgetPageTitlesFollowingSearch": "Naslovi stranica", + "WidgetPageUrlsFollowingSearch": "Stranice", + "WidgetPagesEntry": "Ulazne stranice", + "WidgetPagesExit": "Izlazne stranice", + "WidgetSearchCategories": "Kategorije za pretragu", + "WidgetSearchKeywords": "Ključne reči", + "WidgetSearchNoResultKeywords": "Ključne reči bez rezultata pretrage" + } +} diff --git a/plugins/Actions/lang/sv.json b/plugins/Actions/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..f8c8a1ea2b7e8d89a68368d6a81112b5aa08d5c5 --- /dev/null +++ b/plugins/Actions/lang/sv.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "Åtgärdstyp", + "ActionTypeSegmentHelp": "En typ av händelse, såsom: %s", + "ActionTypes": "Typer av handlingar", + "ActionsInVisit": "Aktiviteter under besök", + "AvgGenerationTimeTooltip": "Medel baseras på %1$s träff(ar) %2$s mellan %3$s och %4$s", + "Behaviour": "Beteende", + "ColumnActionURL": "Åtgärds-URL", + "ColumnClickedURL": "Klickade utlänkar", + "ColumnClickedURLs": "Klickade utlänkar", + "ColumnClicks": "Klick", + "ColumnClicksDocumentation": "Antalet gånger som denna länk klickades på.", + "ColumnDownloadURL": "Nedladdnings-URL", + "ColumnDownloadURLs": "Nerladdade adresser", + "ColumnEntryPageTitle": "Målsidans titel", + "ColumnEntryPageURL": "Målsidans URL", + "ColumnEntryPageURLs": "Ingångssidornas adresser", + "ColumnExitPageTitle": "Avvisningssidans titel", + "ColumnExitPageURL": "Avvisningssidans URL", + "ColumnExitPageURLs": "Utgångssidornas adresser", + "ColumnIdPageview": "Sidvisnings-ID", + "ColumnInteractionPosition": "Interaktionsplats", + "ColumnNoResultKeyword": "Nyckelord utan resultat", + "ColumnPageName": "Sidnamn", + "ColumnPageURL": "Sidans URL", + "ColumnPageViewPosition": "Position för sidvisning", + "ColumnPagesPerSearch": "Sökresultatsidor", + "ColumnPagesPerSearchDocumentation": "Ibland klickar användare som söker på din webbplats på \"nästa\" för att visa flera resultat. Detta är det genomsnittliga antalet sidor med sökresultat som visas för det här nyckelordet.", + "ColumnSearchCategory": "Sökkategori", + "ColumnSearchExits": "%% Sökutgångar", + "ColumnSearchExitsDocumentation": "Procentandelen besök som lämnade webbplatsen efter att ha sökt efter detta nyckelord i din webbplats sökmotor.", + "ColumnSearchResultsCount": "Antal sökresultat", + "ColumnSearches": "Sökningar", + "ColumnSearchesDocumentation": "Antalet besök som sökte efter detta nyckelord i din webbplats sökmotor.", + "ColumnSiteSearchKeywords": "Unika nyckelord", + "ColumnUniqueClicks": "Unika klick", + "ColumnUniqueClicksDocumentation": "Antalet besök som involverade ett klick på denna länk. Om en länk klickades på flera gånger under ett besök, så räknas det bara en gång.", + "ColumnUniqueDownloads": "Unika nedladdningar", + "ColumnUniqueOutlinks": "Unika utlänkar", + "DownloadsReportDocumentation": "I denna rapport kan du se vilka filer som besökarna har laddat ner. %s Vad Matomo räknar som en nedladdning är klicket på en nedladdningslänk. Oavsett om nedladdningen slutfördes eller inte.", + "DownloadsSubcategoryHelp1": "I den här rapporten kan du se vilka filer dina besökare har laddat ner.", + "DownloadsSubcategoryHelp2": "Vad Matomo räknar som en nedladdning är det klick som görs på en nedladdningslänk. Huruvida nedladdningen slutfördes eller inte är inte känt för Matomo.", + "EntryPageTitles": "Målsidors titel", + "EntryPageTitlesReportDocumentation": "Den här rapporten innehåller information om sidtitlar för målsidorna som använts under den valda perioden.", + "EntryPagesReportDocumentation": "Denna rapport innehåller information om ingångssidor som användes under perioden. En ingångssida är den första sida en användare landar på under sitt besök. %s Ingångs-URL:er visas i en katalogstruktur.", + "EntryPagesSubcategoryHelp1": "Denna rapport innehåller information om de ingångssidor som användes under den angivna perioden. En ingångssida är den första sidan som en användare ser under sitt besök.", + "EntryPagesSubcategoryHelp2": "Ingångs-URL:erna visas som en mappstruktur.", + "ExitPageTitles": "Utgångssidors titel", + "ExitPageTitlesReportDocumentation": "Den här rapporten innehåller information om sidtitlar för utgångssidorna som använts under den valda perioden.", + "ExitPagesReportDocumentation": "Denna rapport innehåller information om utgångssidor som användes under perioden. En utgångssida är den sista sida en användare ser på under sitt besök. %sUtgångsside-URL:er visas i en katalogstruktur.", + "ExitPagesSubcategoryHelp1": "Denna rapport innehåller information om utgångssidorna som inträffade under den angivna perioden. En utgångssida är den sista sidan som en användare ser under sitt besök.", + "ExitPagesSubcategoryHelp2": "Utgångs-URL:erna visas som en mappstruktur.", + "MainMetricsReportDocumentation": "Denna rapport ger en mycket grundläggande översikt över vilka åtgärder dina besökare gör på din webbplats.", + "OneSearch": "1 sökning", + "OutlinkDocumentation": "En utlänk är en länk som leder besökaren bort från webbplatsen (till en annan domän).", + "OutlinksReportDocumentation": "Denna rapport visar en hierarkisk lista över utlänkarnas webbadresser som klickades på utav dina besökare.", + "OutlinksSubcategoryHelp1": "Den här rapporten visar en hierarkisk lista över utlänk-URL:er som besökarna klickade på. En utlänk är en länk som leder besökaren bort från din webbplats (till en annan domän).", + "PageTitlesReportDocumentation": "Denna rapport innehåller information om titlarna på de sidor som har besökts. %1$s sidans titel är HTML-taggen %2$s som de flesta webbläsare visar i sina fönster titel.", + "PageTitlesSubcategoryHelp1": "Denna rapport innehåller information om titlarna på de sidor som har besökts.", + "PageTitlesSubcategoryHelp2": "Sidans titel är HTML &lt;title&gt; taggen som de flesta webbläsare visar i deras fönstertitel.", + "PageUrls": "Webbadresser", + "PageViewsByVisitor": "Antal gånger som den här sidan har visats av besökaren, alla besök summerade", + "PagesReportDocumentation": "Denna rapport innehåller information om webbadresserna som har besökts. %s Tabellen ordnas hierarkiskt, webbadresserna visas som en mappstruktur.", + "PagesSubcategoryHelp1": "Denna rapport innehåller information om sidans webbadresser som har besökts.", + "PagesSubcategoryHelp2": "Tabellen är organiserad hierarkiskt, URL:erna visas som en mappstruktur.", + "PagesSubcategoryHelp3": "Använd plus- och minusikonerna till höger för att navigera.", + "PluginDescription": "Rapporter om sidvisningarna och sidtitlarna. Låter dig mäta din interna webbplats sökmotor. Spårar automatiskt klick på externa länkar och nedladdningar av filer.", + "RevisitedPages": "Sidor visade fler än en gång", + "SearchesSegmentHelp": "För att välja alla besök som använde webbplatssökning, använd: %s", + "SiteSearchCategories": "Kategorier (Sajtsökning)", + "SiteSearchCategories1": "Den här rapporten listar kategorierna som besökarna valt när dom gjort en sökning på din webbplats.", + "SiteSearchCategories2": "På e-handelswebbplatser finns det t.ex. vanligtvis en \"Kategori\"-väljare så att besökare kan begränsa sina sökningar till alla produkter i en viss Kategori.", + "SiteSearchCategory": "Kategori (Sajtsökning)", + "SiteSearchFollowingPagesDoc": "När besökare söker på din webbplats så letar dom efter en speciell sida, innehåll, produkt eller tjänst. Den här rapporten listar de sidor med flest klick efter en intern sökning. Med andra ord, en lista med dom mest sökta sidorna av besökare som redan hittat till din webbplats.", + "SiteSearchIntro": "Att spåra sökningar som besökare gör på webbplatsen är ett väldigt effektivt sätt att lära sig mer om vad dom letar efter. Det kan hjälpa till med nya idéer för innehåll eller nya produkter som potentiella kunder letar efter. Det kan även förbättra den allmänna upplevelsen besökarna får på din webbplats.", + "SiteSearchKeyword": "Nyckelord (Webbplatssökning)", + "SiteSearchKeywordCount": "Antal sökord (webbplatssökning)", + "SiteSearchKeywordCounts": "Antal sökord (webbplatssökning)", + "SiteSearchKeywords": "Nyckelord (Sajtsökning)", + "SiteSearchKeywordsDocumentation": "Den här rapporten listar nyckelorden som besökare sökt efter med din interna sökmotor.", + "SiteSearchKeywordsNoResultDocumentation": "Den här rapporten listar sökorden som inte returnerade något sökresultat: kanske kan sökmotoralgoritmen förbättras, eller kanske dina besökare letar efter innehåll som inte (ännu) finns på din webbplats?", + "SiteSearchSubcategoryHelp1": "Sökdelen visar vilka nyckelord besökare använder när de söker på din webbplats. Det visar också vilka sidor användarna går in på efter en sökning och vilka sökord som inte ger några resultat alls.", + "SiteSearchSubcategoryHelp2": "Dessa rapporter kan ge dig en bild av innehåll som saknas på din webbplats, inblick i vad dina besökare letar efter men som de inte hittar så lätt, och mer.", + "SiteSearchSubcategoryHelp3": "Läs mer i webbplatssök-guiden.", + "SubmenuPageTitles": "Sidtitlar", + "SubmenuPagesEntry": "Målsidor", + "SubmenuPagesExit": "Utgångssidor", + "SubmenuSitesearch": "Webbplatssök", + "TimeSpentInReferringAction": "Tid lagd i hänvisningsaktivitet", + "WidgetEntryPageTitles": "Målsidors titlar", + "WidgetExitPageTitles": "Utgångssidors titlar", + "WidgetPageTitles": "Sidtitlar", + "WidgetPageTitlesFollowingSearch": "Sidtitlar från en webbplatssökning", + "WidgetPageUrlsFollowingSearch": "Sidor från en webbplatssökning", + "WidgetPagesEntry": "Målsidor", + "WidgetPagesExit": "Utgångssidor", + "WidgetSearchCategories": "Sök kategorier", + "WidgetSearchKeywords": "Nyckelord från webbplatssökningar", + "WidgetSearchNoResultKeywords": "Nyckelord utan resultat" + } +} diff --git a/plugins/Actions/lang/ta.json b/plugins/Actions/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..d7a832d840285b8ccc20f02fdbadab1a1bf57563 --- /dev/null +++ b/plugins/Actions/lang/ta.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "செயல் வகை", + "ActionTypeSegmentHelp": "ஒரு வகை செயல், இது போன்றது: %s", + "ActionTypes": "செயல் வகைகள்", + "ActionsInVisit": "வருகையில் நடவடிக்கைகள்", + "AvgGenerationTimeTooltip": "%1$s இட் (கள்) %2$s இன் அடிப்படையில் சராசரி %3$s மற்றும் %4$s க்கு இடையில்", + "Behaviour": "நடத்தை", + "ColumnActionURL": "செயல் இணைய முகவரி", + "ColumnClickedURL": "சொடுக்கிய வெளியிணைப்பு", + "ColumnClickedURLs": "சொடுக்கிய வெளியிணைப்புகள்", + "ColumnClicks": "சொடுக்குகள்", + "ColumnClicksDocumentation": "இந்த இணைப்பு சொடுக்குகளின் எண்ணிக்கை.", + "ColumnDownloadURL": "பதிவிறக்க URL", + "ColumnDownloadURLs": "முகவரி களைப் பதிவிறக்கவும்", + "ColumnEntryPageTitle": "நுழைவு பக்க தலைப்பு", + "ColumnEntryPageURL": "நுழைவு பக்க URL", + "ColumnEntryPageURLs": "நுழைவு பக்க முகவரி கள்", + "ColumnExitPageTitle": "வெளியேற பக்க தலைப்பு", + "ColumnExitPageURL": "வெளியேற பக்க URL", + "ColumnExitPageURLs": "பக்க முகவரி களில் இருந்து வெளியேறவும்", + "ColumnIdPageview": "பக்கக் காட்சி ஐடி", + "ColumnInteractionPosition": "தொடர்பு நிலை", + "ColumnNoResultKeyword": "தேடல் முடிவு இல்லாத திறவுச்சொல்", + "ColumnPageName": "பக்கத்தின் பெயர்", + "ColumnPageURL": "பக்கத்தின் URL", + "ColumnPageViewPosition": "பக்கக் காட்சி நிலை", + "ColumnPagesPerSearch": "தேடல் முடிவுகளின் பக்கங்கள்", + "ColumnPagesPerSearchDocumentation": "விருந்தினர்கள் உங்கள் தளத்தின் உள் தேடுவார்கள். சில சமயம் \"அடுத்து\" பகுதி ஊடாக மேலதிக தேடல் முடிவுகளை பார்வையிடுவார்கள். இது குறிச்சொல்லுக்கான தேடல் முடிவுப்பக்கங்களின் பக்க பார்வைக்கான சராசரி.", + "ColumnSearchCategory": "தேடல் பகுப்புகள்", + "ColumnSearchExits": "%% தேடல் வெளியேறுகிறது", + "ColumnSearchExitsDocumentation": "உங்கள் தளத்தில் உள்ள தேடியந்திரத்தில் இந்த குறிச்சொல்லை தேடிய பின்னர் வெளியேறிய பார்வையாளர்களின் வீதம்.", + "ColumnSearchResultsCount": "தேடல் முடிவுகளின் எண்ணிக்கை", + "ColumnSearches": "தேடல்கள்", + "ColumnSearchesDocumentation": "உங்களுடைய இணையத்தளத்தில் உள்ள தேடு பொறியில் , இந்த சொல்லை தேடியவர்களின் எண்ணிக்கை.", + "ColumnSiteSearchKeywords": "தனித்துவமான திறவுச்சொற்கள்", + "ColumnUniqueClicks": "தனித்துவமான சொடுக்குகள்", + "ColumnUniqueClicksDocumentation": "இந்த இணைப்பின் மூலமாக வலை பக்க வருகைகளின் எண்ணிக்கை.ஒரு இணைப்பு ஒரு வலை பக்க வருகைக்காகப் பல முறை சொடுக்கப்படும் என்றால், அது ஒரு முறை மட்டுமே கணக்கிடப்படும்.", + "ColumnUniqueDownloads": "தனித்துவமான பதிவிறக்கங்கள்", + "ColumnUniqueOutlinks": "தனித்துவமான வெளி இணைப்புகள்", + "DownloadsReportDocumentation": "வலை பக்க பார்வையாளர்கள் பதிவிறக்கம் செய்த கோப்புகளின் அறிக்கை. %s பதிவிறக்க கோப்புகளின் எண்ணிக்கை முழுவதுமாக பதிவிறக்கம் செய்யப்பட்ட கோப்புகளின் எண்ணிகையை சார்ந்தது அல்ல. download link சொடுக்குகளின் எண்ணிகையை சார்ந்தது.", + "DownloadsSubcategoryHelp1": "இந்த அறிக்கையில், உங்கள் பார்வையாளர்கள் எந்த கோப்புகளை பதிவிறக்கம் செய்துள்ளனர் என்பதை நீங்கள் காணலாம்.", + "DownloadsSubcategoryHelp2": "பதிவிறக்கமாக மாடோமோ எண்ணுவது பதிவிறக்க இணைப்பைக் சொடுக்கு செய்க. பதிவிறக்கம் நிறைவடைந்ததா இல்லையா என்பது மாடோமோவுக்குத் தெரியவில்லை.", + "EntryPageTitles": "நுழைவு பக்க தலைப்புகள்", + "EntryPageTitlesReportDocumentation": "இந்த அறிக்கை குறிப்பிட்ட காலப்பகுதியில் நுழைவுப்பக்கங்களின் தலைப்புக்களை கொண்ட்டுள்ளது.", + "EntryPagesReportDocumentation": "இந்த அறிக்கையில் குறிப்பிட்ட காலகட்டத்தில் பயன்படுத்தப்பட்ட நுழைவு பக்கங்கள் பற்றிய தகவல்கள் உள்ளன. நுழைவு பக்கம் என்பது ஒரு பயனர் தங்கள் வருகையின் போது பார்க்கும் முதல் பக்கம். %s நுழைவு முகவரி கள் ஒரு கோப்புறை கட்டமைப்பாக காட்டப்படும்.", + "EntryPagesSubcategoryHelp1": "இந்த அறிக்கையில் குறிப்பிட்ட காலகட்டத்தில் பயன்படுத்தப்பட்ட நுழைவு பக்கங்கள் பற்றிய தகவல்கள் உள்ளன. நுழைவு பக்கம் என்பது ஒரு பயனர் தங்கள் வருகையின் போது பார்க்கும் முதல் பக்கம்.", + "EntryPagesSubcategoryHelp2": "நுழைவு முகவரி கள் ஒரு கோப்புறை கட்டமைப்பாக காட்டப்படும்.", + "ExitPageTitles": "வெளியேற பக்க தலைப்புகள்", + "ExitPageTitlesReportDocumentation": "குறிப்பிட்ட காலப்பகுதியில் வெளியேறிய பக்கங்களின் தலைப்புக்களை இந்த அறிக்கை கொண்டுள்ளது.", + "ExitPagesReportDocumentation": "இந்த அறிக்கையில் குறிப்பிட்ட காலகட்டத்தில் ஏற்பட்ட வெளியேறும் பக்கங்கள் பற்றிய தகவல்கள் உள்ளன. வெளியேறும் பக்கம் என்பது ஒரு பயனர் தங்கள் வருகையின் போது பார்க்கும் கடைசி பக்கம். %s வெளியேறும் முகவரி கள் ஒரு கோப்புறை கட்டமைப்பாக காட்டப்படும்.", + "ExitPagesSubcategoryHelp1": "இந்த அறிக்கையில் குறிப்பிட்ட காலகட்டத்தில் ஏற்பட்ட வெளியேறும் பக்கங்கள் பற்றிய தகவல்கள் உள்ளன. வெளியேறும் பக்கம் என்பது ஒரு பயனர் தங்கள் வருகையின் போது பார்க்கும் கடைசி பக்கம்.", + "ExitPagesSubcategoryHelp2": "வெளியேறும் முகவரி கள் ஒரு கோப்புறை கட்டமைப்பாக காட்டப்படும்.", + "MainMetricsReportDocumentation": "உங்கள் பார்வையாளர்கள் உங்கள் வலைத்தளத்தில் என்ன நடவடிக்கைகளை எடுக்கிறார்கள் என்பதற்கான அடிப்படை கண்ணோட்டத்தை இந்த அறிக்கை வழங்குகிறது.", + "OneSearch": "1 தேடல்", + "OutlinkDocumentation": "ஒரு வெளியிணைப்பு என்பது வருகையாளரை உங்கள் தளத்தை விட்டு (வேறு ஒரு ஆட்புல பேருக்கு) அழைத்து செல்லும் பிறிதொரு இணைய முகவரி ஆகும்.", + "OutlinksReportDocumentation": "இந்த அறிக்கை உங்கள் வருகையாளர்களால் வெளி இணைப்பு முகவரிகளில் மேற்கொள்ளப்பட்ட சொடுக்கங்களை படிநிலை முறை நிலையில் காட்டுகிறது.", + "OutlinksSubcategoryHelp1": "இந்த அறிக்கை உங்கள் பார்வையாளர்களால் சொடுக்கு செய்யப்பட்ட அவுட்ல் முகவரி களின் படிநிலை பட்டியலைக் காட்டுகிறது. ஒரு அவுட்லிங்க் என்பது உங்கள் வலைத்தளத்திலிருந்து (மற்றொரு களத்திற்கு) பார்வையாளரை வழிநடத்தும் ஒரு இணைப்பு.", + "PageTitlesReportDocumentation": "இந்த அறிக்கை நீங்கள் இதற்கு முன்பு சென்று வந்த பக்கங்களைப் பற்றிய தலைப்புகளை உள்ளடக்கியது. %1$s ஒரு பக்கத்தின் தலைப்பானது %2$s அனைத்து உலாவிகளின் தலைப்பாக காட்டப்படும் HTML டேக் ஆகும்.", + "PageTitlesSubcategoryHelp1": "இந்த அறிக்கையில் பார்வையிட்ட பக்கங்களின் தலைப்புகள் பற்றிய தகவல்கள் உள்ளன.", + "PageTitlesSubcategoryHelp2": "பக்க தலைப்பு உஉகுமொ &lt;தலைப்பு &gt; பெரும்பாலான உலாவிகள் அவற்றின் சாளர தலைப்பில் காண்பிக்கும் குறிச்சொல்.", + "PageUrls": "பக்க URLs", + "PageViewsByVisitor": "அனைத்து வருகைகளிலும் இந்த பார்வையாளரால் இந்தப் பக்கம் எத்தனை முறை பார்க்கப்பட்டது", + "PagesReportDocumentation": "இந்த அறிக்கை வருகை தரப்பட்ட பக்கங்களின் முகவரிகளை காட்டுகிறது. %s அட்டவணை படிமுறை நிலையில் ஒழுங்காக்கப்பட்டு உள்ளது. முகவரிகள் கோப்பு வடிவில் உள்ளன.", + "PagesSubcategoryHelp1": "இந்த அறிக்கையில் பார்வையிட்ட பக்க முகவரி கள் பற்றிய தகவல்கள் உள்ளன.", + "PagesSubcategoryHelp2": "அட்டவணை படிநிலையாக ஒழுங்கமைக்கப்பட்டுள்ளது, முகவரி கள் ஒரு கோப்புறை கட்டமைப்பாக காட்டப்படும்.", + "PagesSubcategoryHelp3": "Fes servir les icones de més i menys a l'esquerra per navegar.", + "PluginDescription": "பக்கக் காட்சிகள் மற்றும் பக்க தலைப்புகள் பற்றிய அறிக்கைகள். உங்கள் உள் வலைத்தளத்தின் தேடுபொறியை அளவிட உங்களை அனுமதிக்கிறது. வெளிப்புற இணைப்புகள் மற்றும் கோப்பு பதிவிறக்கங்களை தானாக கண்காணிக்கிறது.", + "RevisitedPages": "பக்கங்கள் ஒன்றுக்கு மேற்பட்ட முறை பார்க்கப்பட்டன", + "SearchesSegmentHelp": "அகத் தளத் தேடலைப் பயன்படுத்திய அனைத்து வருகைகளையும் தேர்ந்தெடுக்க, பயன்படுத்தவும்: %s", + "SiteSearchCategories": "வகைகள் (தள தேடல்)", + "SiteSearchCategories1": "இந்த அறிக்கையை, உங்கள் வலைத்தளத்தில் ஒரு தேடல் செய்த போது பார்வையாளர்கள் தேர்ந்தெடுத்த வகைகள் பட்டியலிடுகிறது.", + "SiteSearchCategories2": "எடுத்துக்காட்டாக, மின்வணிக வலைத்தளங்களில் பொதுவாக ஒரு \"வகை\" தேர்வு இருக்கிறது. இதன் மூலம் தேடல் முடிவுகளை குறிப்பிட்ட வகையின் உள் பார்வையாலரால் மட்டுப்படுத்த முடியும்.", + "SiteSearchCategory": "வகை (தள தேடல்)", + "SiteSearchFollowingPagesDoc": "பார்வையாளர் உங்கள் தளத்தில் தேடும் போது, அவர்கள் தனித்தன்மை வாய்ந்த பக்கங்கள், உற்பத்திகள் அல்லது சேவைகளை தேடுகிறார்கள். இந்த அறிக்கை உள்ளக தேடலின் பின்னர், அதிகளவில் பார்க்கப்பட்ட பக்கங்களை காட்டுகிறது.இவை பார்வையாளர்களால் அதிகளவில் தேடப்பட்ட பக்கங்கள் எனவும் சொல்லலாம்.", + "SiteSearchIntro": "உங்கள் வலைத்தளத்தில் பார்வையாளர்கள் செய்யும் தேடல்களைக் கண்காணிப்பது உங்கள் பார்வையாளர்கள் எதைத் தேடுகிறது என்பதைப் பற்றி மேலும் அறிய மிகவும் பயனுள்ள வழியாகும், இது புதிய உள்ளடக்கத்திற்கான யோசனைகளைக் கண்டறிய உதவும், சாத்தியமான வாடிக்கையாளர்களைத் தேடக்கூடிய புதிய இணையவழி தயாரிப்புகள் மற்றும் பொதுவாக பார்வையாளர்களை மேம்படுத்துதல் உங்கள் இணையதளத்தில் பட்டறிவு.", + "SiteSearchKeyword": "தேடுசொல் (தள தேடுபொறி)", + "SiteSearchKeywordCount": "முக்கிய எண்ணிக்கை (தள தேடல்)", + "SiteSearchKeywordCounts": "முக்கிய எண்ணிக்கைகள் (தள தேடல்)", + "SiteSearchKeywords": "முக்கிய வார்த்தைகள் (தள தேடல்)", + "SiteSearchKeywordsDocumentation": "இந்த அறிக்கை உள்ளக தேடலில் பயன்பட்ட சொற்களை பட்டியல் இடுகிறது.", + "SiteSearchKeywordsNoResultDocumentation": "எந்தவொரு தேடல் முடிவையும் திருப்பித் தராத தேடல் முக்கிய வார்த்தைகளை இந்த அறிக்கை பட்டியலிடுகிறது: ஒருவேளை தேடுபொறி வழிமுறையை மேம்படுத்தலாம், அல்லது உங்கள் பார்வையாளர்கள் உங்கள் வலைத்தளத்தில் (இன்னும்) இல்லாத உள்ளடக்கத்தைத் தேடுகிறார்களா?", + "SiteSearchSubcategoryHelp1": "உங்கள் வலைத்தளத்தைத் தேடும்போது பார்வையாளர்கள் எந்த முக்கிய வார்த்தைகளைப் பயன்படுத்துகிறார்கள் என்பதை தள தேடல் பிரிவு காட்டுகிறது. ஒரு தேடலைச் செய்தபின் பயனர்கள் எந்த பக்கங்களைப் பார்க்கிறார்கள் என்பதையும், எந்த ஆன்-சைட் தேடல் முக்கிய வார்த்தைகள் எந்த முடிவுகளையும் திருப்பித் தரும் என்பதையும் இது காட்டுகிறது.", + "SiteSearchSubcategoryHelp2": "இந்த அறிக்கைகள் உங்கள் தளத்தில் உள்ளடக்கத்தைக் காணவில்லை, உங்கள் பார்வையாளர்கள் எதைத் தேடுகிறார்கள் என்பதைப் பற்றிய யோசனைகளை உங்களுக்கு வழங்க முடியும், ஆனால் எளிதாகக் கண்டுபிடிக்க முடியவில்லை, மேலும் பல.", + "SiteSearchSubcategoryHelp3": "தள தேடல் வழிகாட்டியில் மேலும் அறிக.", + "SubmenuPageTitles": "பக்க தலைப்புக்கள்", + "SubmenuPagesEntry": "நுழைவு பக்கங்கள்", + "SubmenuPagesExit": "வெளியேற பக்கங்கள்", + "SubmenuSitesearch": "தளத்தின் தேடல்கள்", + "TimeSpentInReferringAction": "செயலைக் குறிப்பிடுவதில் செலவழித்த நேரம்", + "WidgetEntryPageTitles": "நுழைவு பக்க தலைப்புக்கள்", + "WidgetExitPageTitles": "வெளியேற பக்க தலைப்புக்கள்", + "WidgetPageTitles": "பக்க தலைப்புக்கள்", + "WidgetPageTitlesFollowingSearch": "இணையதளத்தேடலை தொடர்ந்த பக்க தலைப்பு", + "WidgetPageUrlsFollowingSearch": "தள தேடலை தொடர்ந்த பக்கங்கள்", + "WidgetPagesEntry": "நுழைவு பக்கங்கள்", + "WidgetPagesExit": "வெளியேற பக்கங்கள்", + "WidgetSearchCategories": "தேடல் பகுப்புகள்", + "WidgetSearchKeywords": "தளத்தின் தேடல் திறவு சொற்கள்", + "WidgetSearchNoResultKeywords": "முடிவுகள் இல்லாத தேடல் திறவு சொற்கள்" + } +} diff --git a/plugins/Actions/lang/te.json b/plugins/Actions/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..c671713f11a06d3c2e148ac213e2e3ab6b290a05 --- /dev/null +++ b/plugins/Actions/lang/te.json @@ -0,0 +1,29 @@ +{ + "Actions": { + "ColumnClicks": "నొక్కులు", + "ColumnClicksDocumentation": "ఎన్నిసార్లు ఈ లింక్ క్లిక్ చేయబడింది", + "ColumnDownloadURL": "దింపుకోలు URL", + "ColumnEntryPageTitle": "చర్యలు", + "ColumnEntryPageURL": "పేజీ URL ను ఎంటర్ చేయండి", + "ColumnExitPageTitle": "Exit పేజి పేరు", + "ColumnExitPageURL": "Exit పేజి URL", + "ColumnPageName": "పుట పేరు", + "ColumnPageURL": "పుట చిరునామా", + "ColumnPagesPerSearch": "శోధన ఫలితాల పేజీలు", + "ColumnSearchCategory": "శోధన వర్గం", + "ColumnSearchResultsCount": "శోధన ఫలితాల కౌంట్", + "ColumnSearches": "శోధనలు", + "ColumnSiteSearchKeywords": "ప్రత్యేక కీవర్డ్లు", + "ColumnUniqueClicks": "ప్రత్యేక క్లిక్", + "ColumnUniqueDownloads": "ప్రత్యేక డౌన్ లోడ్", + "ColumnUniqueOutlinks": "డౌన్ లోడ్", + "EntryPageTitles": "ఎంట్రీ పేజీ శీర్షికలు", + "ExitPageTitles": "నిష్క్రమణ పేజీ శీర్షికలు", + "OneSearch": "1 శోధన", + "PageUrls": "పేజీ URL లు", + "SubmenuPageTitles": "పుటల శీర్షికలు", + "SubmenuPagesEntry": "ప్రవేశ పుటలు", + "SubmenuPagesExit": "నిష్క్రమణ పుటలు", + "WidgetPageTitles": "పేజీ శీర్షికలు" + } +} diff --git a/plugins/Actions/lang/th.json b/plugins/Actions/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..6a77b1def1799deaca7ba7e32b3f0e09c8750ba4 --- /dev/null +++ b/plugins/Actions/lang/th.json @@ -0,0 +1,47 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "ค่าเฉลี่ยของจำนวน %1$s ครั้ง %2$s ระหว่าง %3$s และ %4$s", + "ColumnClicks": "คลิก", + "ColumnClicksDocumentation": "จำนวนครั้งที่ link นี้ถูกคลิก", + "ColumnDownloadURL": "URL ที่ถูกดาว์นโหลด", + "ColumnEntryPageTitle": "หัวเรื่องของหน้าขาเข้า", + "ColumnEntryPageURL": "URL ของหน้าขาเข้า", + "ColumnExitPageTitle": "หัวเรื่องของหน้าขาออก", + "ColumnExitPageURL": "URL ของหน้าขาออก", + "ColumnNoResultKeyword": "Keyword ที่ไม่มีผลการค้นหา", + "ColumnPageName": "ชื่อหน้า", + "ColumnPageURL": "URL หน้า", + "ColumnPagesPerSearch": "หน้าที่แสดงบนผลการค้นหา", + "ColumnPagesPerSearchDocumentation": "ในบางครั้งผู้เข้าชมที่ค้นหาบนเว็บไซต์ของคุณ จะกด \"ถัดไป\" เพื่อดูผลลัพธ์ที่มากขึ้น สิ่งนี้เป็นค่าเฉลี่ยของจำนวนหน้าผลการค้นหาที่ถูกรับชมสำหรับ Keyword นี้", + "ColumnSearchCategory": "หมวดหมู่การค้นหา", + "ColumnSearchExitsDocumentation": "ร้อยละของผู้เข้าชมที่ออกเว็บไซต์หลังค้นหาด้วย keyword นี้บนเครื่องมือค้นหาของเว็บไซต์ของคุณ", + "ColumnSearchResultsCount": "จำนวนผลการค้นหา", + "ColumnSearches": "ค้นหา", + "ColumnSearchesDocumentation": "จำนวนผู้เข้าชมที่ค้นหา Keyword นี้บนระบบค้นหาในเว็บไซต์ของคุณ", + "ColumnSiteSearchKeywords": "Keyword ที่ไม่ซ้ำ", + "ColumnUniqueClicks": "คลิกที่ไม่ซ้ำ", + "ColumnUniqueClicksDocumentation": "จำนวนการเข้าชมที่เกี่ยวข้องกับการคลิกที่ลิงค์นี้ หากการเชื่อมโยงการคลิกหลายครั้งในระหว่างเข้าชมครั้งเดียวก็จะถูกนับเพียงครั้งเดียว", + "ColumnUniqueDownloads": "ดาวน์โหลดที่ไม่ซ้ำ", + "ColumnUniqueOutlinks": "ลิงค์ออกที่ไม่ซ้ำ", + "DownloadsReportDocumentation": "ในรายงานนี้ คุณจะสามารถทราบว่าไฟล์ใดได้ถูกผู้เข้าชมดาว์นโหลดไป %s Matomo จะนับดาวน์โหลดจากการคลิกที่ลิงค์ดาวน์โหลด อย่างไรก็ตามดาวน์โหลดได้เสร็จสิ้นหรือไม่นั้น Matomo ไม่สามารถรู้ได้", + "EntryPageTitles": "หัวเรื่องหน้าขาเข้า", + "EntryPageTitlesReportDocumentation": "รายงานนี้ประกอบด้วยข้อมูลเกี่ยวกับหัวเรื่องของหน้าขาเข้า ที่ถูกเรียกใช้ในช่วงระยะเวลาที่กำหนดไว้", + "ExitPageTitles": "หัวเรื่องหน้าขาออก", + "ExitPageTitlesReportDocumentation": "รายงานนี้ประกอบด้วยข้อมูลเกี่ยวกับหัวเรื่องของหน้าขาออก ที่เกิดขึ้นระหว่างระยะเวลาที่กำหนดไว้", + "OneSearch": "1 ค้นหา", + "OutlinkDocumentation": "Outlink คือการเชื่อมโยงที่นำไปสู่ผู้เข้าชมอยู่ห่างจากเว็บไซต์ของคุณ (โดเมนอื่น)", + "OutlinksReportDocumentation": "รายงานนี้จะแสดงรายการลำดับชั้นของการเชื่อมโยง URL อัตโนมัติ ที่มีการคลิกโดยผู้เข้าชมของคุณ", + "PageTitlesReportDocumentation": "รายงานนี้ประกอบด้วยข้อมูลเกี่ยวกับชื่อเรื่องของหน้าเว็บที่ได้รับการเข้าเยี่ยมชม %1$s ชื่อหน้าจะเป็นแท็ก %2$s HTML เบราว์เซอร์ส่วนใหญ่จะแสดงในชื่อเรื่องของหน้าต่างของพวกเขา", + "PageUrls": "Url ของเพจ", + "PagesReportDocumentation": "รายงานนี้ประกอบด้วยข้อมูลเกี่ยวกับ URL ของหน้าที่ได้รับการเข้าเยี่ยมชม %s ตารางจะถูกจัดลำดับชั้น URL ที่จะปรากฏเป็นโครงสร้างโฟลเดอร์", + "PagesSubcategoryHelp3": "ใช้ไอคอนบวกและลบที่อยู่ด้านซ้ายเพื่อเป็นตัวนำทาง", + "SiteSearchCategories1": "รายงานนี้แสดงรายการหมวดหมู่ที่ผู้เข้าชมเลือกไว้ เมื่อพวกเขาทำค้นหาบนเว็บไซต์ของคุณ", + "SiteSearchCategories2": "ตัวอย่างเช่น เว็บไซต์อีคอมเมิร์ซมักจะมี \"หมวดหมู่\" เพื่อให้ผู้เข้าชมสามารถจำกัดการค้นหาของพวกเขาให้กับสินค้าทั้งหมดในหมวดหมู่ที่เลือกไว้ได้", + "SiteSearchFollowingPagesDoc": "เมื่อผู้เยี่ยมชมค้นหาเว็บไซต์ของคุณ พวกเขาจะค้นหาเฉพาะหน้า เนื้อหา ผลิตภัณฑ์ หรือบริการ รายงานนี้จะแสดงรายการหน้าที่ถูกคลิกเป็นส่วนใหญ่", + "SiteSearchKeyword": "คีย์เวิร์ด (ค้นหาด้วยเว็บไซต์)", + "SubmenuPageTitles": "ชื่อเพจ", + "SubmenuPagesEntry": "รายการเพจ", + "SubmenuPagesExit": "ออกจากหน้าเพจ", + "WidgetPagesExit": "ออกจากเพจ" + } +} diff --git a/plugins/Actions/lang/tl.json b/plugins/Actions/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..9a796072922118268035e7dd98815ca1ccbe61fb --- /dev/null +++ b/plugins/Actions/lang/tl.json @@ -0,0 +1,59 @@ +{ + "Actions": { + "AvgGenerationTimeTooltip": "Pamantayan batay sa mga %1$s ng mga hit %2$s sa pagitan ng %3$s at %4$s", + "ColumnClicks": "Mga Click", + "ColumnClicksDocumentation": "Kung ilang beses na na-click ang link na ito.", + "ColumnDownloadURL": "URL para ma-download", + "ColumnEntryPageTitle": "Pamagat ng Entry Page", + "ColumnEntryPageURL": "URL ng Entry Page", + "ColumnExitPageTitle": "Pamagat ng Exit Page", + "ColumnExitPageURL": "URL ng Exit Page", + "ColumnNoResultKeyword": "Keyword na Walang Resulta sa Paghahanap", + "ColumnPageName": "Pangalan ng Pahina", + "ColumnPageURL": "URL ng Pahina", + "ColumnPagesPerSearch": "Mga pahina ng mga Resulta sa Paghahanap", + "ColumnPagesPerSearchDocumentation": "Dokumentasyon ng bawat nahahanap na pahina", + "ColumnSearchCategory": "Kategorya ng Paghahanap", + "ColumnSearchExitsDocumentation": "Ang porsyento ng mga pagbisita na iniwanan ang website pagkatapos maghanap ng Keyword na ito sa iyong engine ng paghahanap sa iyong site.", + "ColumnSearchResultsCount": "Bilang ng Mga Resulta sa Paghahanap", + "ColumnSearches": "Mga Paghahanap", + "ColumnSearchesDocumentation": "Ang bilang ng mga bisita na naghanap para sa keyword na ito sa engine ng paghahanap ng iyong website.", + "ColumnSiteSearchKeywords": "Natatanging mga Keyword", + "ColumnUniqueClicks": "Natatanging mga Pag-click", + "ColumnUniqueClicksDocumentation": "Ang bilang ng pagbisita na nakapaloob sa pag-click sa link na ito. Kung ang isang link ay na-click ng maraming beses sa panahon ng isang pagbisita, ito ay binibilang lamang nang isang beses.", + "ColumnUniqueDownloads": "Natatanging mga Download", + "ColumnUniqueOutlinks": "Natatanging mga Outlink", + "DownloadsReportDocumentation": "Sa ulat na ito, maaari mong makita kung aling mga file ang i-dinownload ng iyong mga bisita . %s Ang binibilang ng Matomo bilang download ay ang pag-click sa download link. Kung ang pag-download ay natapos o hindi ito ay hindi na nalalaman ng Matomo.", + "EntryPageTitles": "Mga pamagat ng entry page", + "EntryPageTitlesReportDocumentation": "Ang ulat na ito ay naglalaman ng impormasyon tungkol sa mga pamagat ng mga 'entry page' na ginamit sa tinukoy na panahon.", + "ExitPageTitles": "Mga pamagat ng exit page", + "ExitPageTitlesReportDocumentation": "Ang ulat na ito ay naglalaman ng impormasyon tungkol sa mga pamagat ng mga 'exit page' na ginamit sa tinukoy na panahon.", + "OneSearch": "1 paghahanap", + "OutlinkDocumentation": "Ang isang outlink ay isang link na naghahatid sa isang bisita nang palayo mula sa iyong website (sa isa pang domain).", + "OutlinksReportDocumentation": "Ipinapakita ng ulat na ito ang hirerkiya na listahan ng mga outlink URL na na-click ng iyong mga bisita.", + "PageTitlesReportDocumentation": "Ang ulat na ito ay naglalaman ng impormasyon tungkol sa mga pamagat ng mga pahina na binisita na. %1$s Ang pamagat ng pahina ay ang HTML %2$s Tag na karaniwang pinapakita ng ng karamihan sa mga browser sa kanilang mga window bilang pamagat.", + "PageUrls": "Mga URL ng Pahina", + "PagesReportDocumentation": "Ang ulat na ito ay naglalaman ng impormasyon tungkol sa mga URL ng pahina na binisita mo. %s ang talahanayan ay nakaayos ng hierarchically, ang mga URL ay ipinapakita bilang isang istraktura ng folder.", + "SiteSearchCategories1": "Inililista ng ulat na ito ang mga kategorya na napili ng mga bisita kapag ginawa nila ang isang Paghahanap sa iyong website.", + "SiteSearchCategories2": "Halimbawa, ang mga Ecommerce website ay karaniwang may tagapili ng \"Kategorya\" upang ang mga bisita ay maaaring malimitahan ang kanilang mga paghahanap sa lahat ng produkto sa isang espesipikong Kategorya.", + "SiteSearchFollowingPagesDoc": "Kapag naghanap ang mga bisita sa iyong website, ang mga ito ay naghahanap para sa isang partikular na pahina, nilalaman, produkto, o serbisyo. Inililista ng ulat na ito ang mga pahina na pinakana-click pagkatapos ng isang internal na paghahanap. Sa isang salita, ang listahan ng mga pahina ang pinaka-hinanap ng mga bisita na sa iyong website.", + "SiteSearchIntro": "Ang pagsubaybay sa mga paghahanap ng mga bisita sa iyong website ay isang napaka-epektibong paraan upang matuto nang higit pa tungkol sa kung ano ang hinahanap ng iyong mga tagasubaybay, ito ay makakatulong na makahanap ng mga ideya para sa mga bagong nilalaman, mga bagong produkto ng Ecommerce na maaaring hinahanap ng potensyal na customer, at sa pangkalahatan ay mapahusay ang karanasan ng mga bisita sa iyong website.", + "SiteSearchKeyword": "Keyword (Paghahanap sa Site)", + "SiteSearchKeywordsDocumentation": "Inililista ng ulat na ito ang mga Keyword sa Paghahanap na hinanap ng mga bisita sa iyong panloob na Engine ng Paghahanap", + "SiteSearchKeywordsNoResultDocumentation": "Inililista ng ulat na ito ang mga Keyword sa Paghahanap na hindi nagpapakita ng anumang resulta ng paghahanap: maari na ang algorithm ng search engine ay pinabuti, o maaring ang iyong mga bisita ay naghahanap ng bagay na wala (pa) sa iyong website?", + "SubmenuPageTitles": "Mga pamagat ng pahina", + "SubmenuPagesEntry": "Mga entry page", + "SubmenuPagesExit": "Mga exit page", + "SubmenuSitesearch": "Paghahanap sa Site", + "WidgetEntryPageTitles": "Mga pamagat ng entry page", + "WidgetExitPageTitles": "Mga pamagat ng exit page", + "WidgetPageTitles": "Mga pamagat ng pahina", + "WidgetPageTitlesFollowingSearch": "Mga Pamagat ng Pahina Pagkatapos ng isang Paghahanap sa Site", + "WidgetPageUrlsFollowingSearch": "Mga Pahina Pagkatapos ng isang Paghahanap sa Site", + "WidgetPagesEntry": "Mga entry page", + "WidgetPagesExit": "Mga exit page", + "WidgetSearchCategories": "Mga Kategorya ng Paghahanap", + "WidgetSearchKeywords": "Mga Keyword ng Paghahanap sa Site", + "WidgetSearchNoResultKeywords": "Paghahanap ng mga Keyword na Walang mga Resulta" + } +} diff --git a/plugins/Actions/lang/tr.json b/plugins/Actions/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..daebb8db0f109147f5bc9e89f45c7371180b331b --- /dev/null +++ b/plugins/Actions/lang/tr.json @@ -0,0 +1,102 @@ +{ + "Actions": { + "ActionType": "İşlem türü", + "ActionTypeSegmentHelp": "Bir işlem türü. Şunun gibi: %s", + "ActionTypes": "İşlem türleri", + "ActionsInVisit": "Ziyaret sırasındaki işlemler", + "AvgGenerationTimeTooltip": "Ortalama %1$s %2$s ortalama erişilmeye göre %3$s ile %4$s arasında", + "Behaviour": "Davranış", + "ColumnActionURL": "İşlem adresi", + "ColumnClickedURL": "Tıklanmış dış bağlantı", + "ColumnClickedURLs": "Tıklanmış dış bağlantılar", + "ColumnClicks": "Tıklanma", + "ColumnClicksDocumentation": "Bu bağlantıya tıklanma sayısı.", + "ColumnDownloadURL": "İndirme adresi", + "ColumnDownloadURLs": "İndirme adresi", + "ColumnEntryPageTitle": "Giriş sayfası başlığı", + "ColumnEntryPageURL": "Giriş sayfası adresi", + "ColumnEntryPageURLs": "Giriş sayfası adresi", + "ColumnExitPageTitle": "Çıkış sayfası başlığı", + "ColumnExitPageURL": "Çıkış sayfası adresi", + "ColumnExitPageURLs": "Çıkış sayfası adresi", + "ColumnIdPageview": "Sayfa gösterimi kimliği", + "ColumnInteractionPosition": "Etkileşim konumu", + "ColumnNoResultKeyword": "Arama sonucu olmayan anahtar sözcük", + "ColumnPageName": "Sayfa adı", + "ColumnPageURL": "Sayfa adresi", + "ColumnPageViewPosition": "Sayfa gösterimi konumu", + "ColumnPagesPerSearch": "Arama sonuçları sayfaları", + "ColumnPagesPerSearchDocumentation": "Ziyaretçiler sitenizde arama yaptıklarında \"Sonraki\" üzerine tıklayarak daha fazla sonuç görüntüleyebilir. Bu sayı, bu anahtar sözcük için görüntülenen ortalama arama sonuç sayfalarının sayısıdır.", + "ColumnSearchCategory": "Arama kategorisi", + "ColumnSearchExits": "%% arama çıkışı", + "ColumnSearchExitsDocumentation": "Bu anahtar sözcüğü sitenizin arama motorunda aradıktan sonra siteden ayrılanların yüzdesi.", + "ColumnSearchResultsCount": "Arama sonucu sayısı", + "ColumnSearches": "Aramalar", + "ColumnSearchesDocumentation": "Bu anahtar sözcük ile sitenin arama motorunda yapılan aramaların sayısı.", + "ColumnSiteSearchKeywords": "Tekil anahtar sözcükler", + "ColumnUniqueClicks": "Tekil tıklamalar", + "ColumnUniqueClicksDocumentation": "Bu bağlantıyı tıklayan ziyaretçilerin sayısı. Bir bağlantı bir ziyarette birden çok kez tıklanmış ise yalnızca bir kez sayılır.", + "ColumnUniqueDownloads": "Tekil indirmeler", + "ColumnUniqueOutlinks": "Tekil dış bağlantılar", + "DownloadsReportDocumentation": "Bu rapor, ziyaretçilerin dosya indirme bilgilerini içerir. %s indirme bağlantısına tıklandığında Matomo indirme sayısını arttırır. İndirmenin tamamlanıp tamamlanmadığı Matomo tarafından bilinmez.", + "DownloadsSubcategoryHelp1": "Bu rapor, ziyaretçilerin dosya indirme bilgilerini içerir.", + "DownloadsSubcategoryHelp2": "İndirme bağlantısına tıklandığında Matomo indirme sayısını arttırır. İndirmenin tamamlanıp tamamlanmadığı Matomo tarafından bilinmez.", + "EntryPageTitles": "Giriş sayfası başlıkları", + "EntryPageTitlesReportDocumentation": "Bu rapor, belirtilen zaman aralığında kullanılan giriş sayfalarının başlıkları hakkında bilgi içerir.", + "EntryPagesReportDocumentation": "Bu rapor, belirtilen zaman aralığında kullanılan giriş sayfaları hakkında bilgi içerir. Bir giriş sayfası, kullanıcının ziyaretine başladığı ilk sayfadır. %s giriş adresleri bir klasör yapısı biçiminde görüntülenir.", + "EntryPagesSubcategoryHelp1": "Bu rapor, belirtilen zaman aralığında kullanılan giriş sayfaları hakkında bilgi içerir. Bir giriş sayfası, kullanıcının ziyaretine başladığı ilk sayfadır.", + "EntryPagesSubcategoryHelp2": "Giriş adresleri bir klasör yapısı biçiminde görüntülenir.", + "ExitPageTitles": "Çıkış sayfası başlıkları", + "ExitPageTitlesReportDocumentation": "Bu rapor, belirtilen zaman aralığında kullanılan çıkış sayfalarının başlıkları hakkında bilgi içerir.", + "ExitPagesReportDocumentation": "Bu rapor, belirtilen zaman aralığında kullanılan çıkış sayfaları hakkında bilgi içerir. Bir çıkış sayfası, kullanıcının ziyaretini bitirdiği son sayfadır. %s çıkış adresleri bir klasör yapısı biçiminde görüntülenir.", + "ExitPagesSubcategoryHelp1": "Bu rapor, belirtilen zaman aralığında kullanılan çıkış sayfaları hakkında bilgi içerir. Bir çıkış sayfası, kullanıcının ziyaretini bitirdiği son sayfadır.", + "ExitPagesSubcategoryHelp2": "Çıkış adresleri bir klasör yapısı biçiminde görüntülenir.", + "MainMetricsReportDocumentation": "Bu rapor, ziyaretçilerin sitede yaptığı işlemlerin çok basit bir özetini içerir.", + "OneSearch": "1 arama", + "OutlinkDocumentation": "Dış bağlantı, ziyaretçinin siteden ayrılmasını sağlayan (başka bir etki alanına) bir bağlantıdır.", + "OutlinksReportDocumentation": "Bu rapor, ziyaretçilerin tıkladığı dış bağlantıların hiyerarşik listesini içerir.", + "OutlinksSubcategoryHelp1": "Bu rapor, ziyaretçilerin tıkladığı dış bağlantıların hiyerarşik listesini içerir. Dış bağlantılar, ziyaretçileri siteden dışarı (başka bir etki alanına) götüren bağlantılardır.", + "PageTitlesReportDocumentation": "Bu rapor, ziyaret edilen sayfaların başlıkları ile ilgili bilgi içerir. %1$s Sayfa başlığı, birçok tarayıcının pencere başlığında gösterdiği %2$s HTML etiketidir.", + "PageTitlesSubcategoryHelp1": "Bu rapor, ziyaret edilen sayfa başlıkları hakkında bilgi içerir.", + "PageTitlesSubcategoryHelp2": "Sayfa başlığı HTML &lt;title&gt; etiketidir. Çoğu tarayıcı bu etiketin içeriğini kendi penceresinin başlığı olarak görüntüler.", + "PageUrls": "Sayfa adresleri", + "PageViewsByVisitor": "Tüm ziyaretleri sırasında bu sayfanın bu ziyaretçi tarafından görüntülenme sayısı", + "PagesReportDocumentation": "Bu rapor, ziyaret edilen sayfa adresleri hakkında bilgi içerir. %s Tablo hiyerarşik olarak düzenlenmiştir, adresler klasör yapısında görüntülenir.", + "PagesSubcategoryHelp1": "Bu rapor, ziyaret edilen sayfa adresleri hakkında bilgi içerir.", + "PagesSubcategoryHelp2": "Tablo hiyerarşik olarak düzenlenmiştir, adresler klasör yapısında görüntülenir.", + "PagesSubcategoryHelp3": "Gezinmek için soldaki artı ve eksi simgelerini kullanın.", + "PluginDescription": "Bu raporlar, sayfa ziyaretleri ve sayfa başlıkları hakkında bilgiler içerir. Sitenin iç arama motorunun ölçülebilmesini sağlar. Dış bağlantı ve dosya indirme bağlantılarına tıklamalar otomatik olarak izlenir.", + "RevisitedPages": "Bir kereden çok görüntülenen sayfalar", + "SearchesSegmentHelp": "Sitenin iç aramasını kullanan tüm ziyaretleri seçmek için şunu kullanın: %s", + "SiteSearchCategories": "Kategori (site araması)", + "SiteSearchCategories1": "Bu rapor, ziyaretçilerin sitede arama yaparken seçtiği kategori bilgilerini içerir.", + "SiteSearchCategories2": "Örnek olarak, e-ticaret sitelerinde tipik bir \"Kategori\" seçicisi bulunur. Ziyaretçiler bu seçiciyi kullanarak aramalarını belirli bir kategori içindeki ürünleri görüntüleyecek şekilde süzebilir.", + "SiteSearchCategory": "Kategori (site araması)", + "SiteSearchFollowingPagesDoc": "Ziyaretçileriniz sitede arama yaptığında belirli bir sayfa, içerik, ürün ya da hizmete bakarlar. Bu rapor, site içinde yapılan bir aramadan sonra en çok tıklanan sayfaları listeler. Diğer bir deyişle, zaten sitede bulunan ziyaretçilerin en çok aradığı sayfaları görüntüler.", + "SiteSearchIntro": "Sitenizde yapılan aramaları izlemek, hedef kitlenizin ne aradığını anlamak için çok etkili bir yoldur. Yeni içerik fikirleri ve potansiyel müşterilerin arayabileceği e-ticaret ürünlerini bulmanızı sağlayarak ziyaretçilerin sitedeki deneyimlerini iyileştirmenizi sağlar.", + "SiteSearchKeyword": "Anahtar sözcük (site araması)", + "SiteSearchKeywordCount": "Anahtar sözcük sayısı (site arama)", + "SiteSearchKeywordCounts": "Anahtar sözcük sayısı (site arama)", + "SiteSearchKeywords": "Anahtar sözcük (site araması)", + "SiteSearchKeywordsDocumentation": "Bu rapor, ziyaretçilerin sitedeki arama motorunda aradığı anahtar sözcükleri içerir.", + "SiteSearchKeywordsNoResultDocumentation": "Bu rapor, herhangi bir sonuç vermeyen arama anahtar sözcüklerini içerir. Arama motoru algoritmasının geliştirilmesi gerekiyor ya da ziyaretçiler (henüz) sitenizde olmayan bir içeriği arıyor olabilir mi?", + "SiteSearchSubcategoryHelp1": "Site arama bölümünde, ziyaretçilerin sitede arama yaparken kullandığı anahtar sözcükler görüntülenir. Ayrıca, kullanıcıların bir arama yaptıktan sonra açtığı sayfalar ile site içi aramasında hiçbir sonuç döndürmeyen anahtar sözcükler de görüntülenir.", + "SiteSearchSubcategoryHelp2": "Bu raporlar, sitede eksik içerikler ile ziyaretçilerin aradığı ancak kolayca bulamadığı şeyler hakkında fikir vermek ve içgörü kazanmak için kullanılabilir.", + "SiteSearchSubcategoryHelp3": "Ayrıntılı bilgi almak için site arama rehberine bakabilirsiniz.", + "SubmenuPageTitles": "Sayfa başlıkları", + "SubmenuPagesEntry": "Giriş sayfaları", + "SubmenuPagesExit": "Çıkış sayfaları", + "SubmenuSitesearch": "Site araması", + "TimeSpentInReferringAction": "Belirtilen işleme harcanan zaman", + "WidgetEntryPageTitles": "Giriş sayfası başlıkları", + "WidgetExitPageTitles": "Çıkış sayfası başlıkları", + "WidgetPageTitles": "Sayfa başlıkları", + "WidgetPageTitlesFollowingSearch": "Site aramasını izleyen sayfa başlıkları", + "WidgetPageUrlsFollowingSearch": "Site aramasını izleyen sayfalar", + "WidgetPagesEntry": "Giriş sayfaları", + "WidgetPagesExit": "Çıkış sayfaları", + "WidgetSearchCategories": "Arama kategorileri", + "WidgetSearchKeywords": "Site arama anahtar sözcükleri", + "WidgetSearchNoResultKeywords": "Sonuç vermeyen arama anahtar sözcükleri" + } +} diff --git a/plugins/Actions/lang/tzm.json b/plugins/Actions/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..0b084e6bb580e38b055723c13241d9096b2d2f26 --- /dev/null +++ b/plugins/Actions/lang/tzm.json @@ -0,0 +1,10 @@ +{ + "Actions": { + "ColumnPageName": "ⴰⵙⵙⴰⵖ ⵏ ⵜⴰⵙⵏⴰ", + "ColumnPageURL": "ⵜⴰⵙⵏⴰ ⵏ URL", + "ColumnSearches": "ⴰⵔⵣⵣⵓ", + "SubmenuSitesearch": "ⴰⵔⵣⵣⵓ ⴷⵉ ⵓⵣⴷⴰⵢ", + "WidgetPageTitles": "ⵉⵣⵡⵉⵍⵏ ⵏ ⵜⴰⵙⵏⴰ", + "WidgetSearchCategories": "ⴰⵔⵣⵣⵓ ⵅⴼ ⵜⴰⴳⴳⴰⵢⵉⵏ" + } +} diff --git a/plugins/Actions/lang/uk.json b/plugins/Actions/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..88c767ec3bc78906b860c06aaab1a78005263945 --- /dev/null +++ b/plugins/Actions/lang/uk.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "Тип дії", + "ActionTypes": "Типи дії", + "ActionsInVisit": "Дії у візиті", + "AvgGenerationTimeTooltip": "В середньому по %1$s хіт (ів) %2$s між %3$s і %4$s", + "Behaviour": "Поведінка", + "ColumnActionURL": "URL дій", + "ColumnClickedURL": "Натиснуто вихідне посилання", + "ColumnClickedURLs": "Натиснуто вихідні посилання", + "ColumnClicks": "Кліків", + "ColumnClicksDocumentation": "Кількість кліків по цьому посиланню.", + "ColumnDownloadURL": "Посилання для завантаження", + "ColumnDownloadURLs": "URL завантажень", + "ColumnEntryPageTitle": "Заголовок початкової сторінки", + "ColumnEntryPageURL": "URL початкової сторінки", + "ColumnEntryPageURLs": "URL сторінки входу", + "ColumnExitPageTitle": "заголовок сторінки виходу", + "ColumnExitPageURL": "URL сторінки виходу", + "ColumnExitPageURLs": "URL сторінки виходу", + "ColumnIdPageview": "ID перегляду", + "ColumnInteractionPosition": "Позиція взаємодії", + "ColumnNoResultKeyword": "Ключове слово без результатів в пошуку", + "ColumnPageName": "Заголовок сторінки", + "ColumnPageURL": "URL сторінок", + "ColumnPageViewPosition": "Позиція перегляду сторінки", + "ColumnPagesPerSearch": "Сторінки результатів пошуку", + "ColumnPagesPerSearchDocumentation": "Відвідувачі будуть користуватися пошуком на вашому сайті і іноді натискати \"next\" (наступна сторінка), щоб переглянути більше результатів. Це середнє число сторінок з результатами пошуку для цього ключового слова.", + "ColumnSearchCategory": "Шукати категорію", + "ColumnSearchExits": "%% Виходів після пошуку", + "ColumnSearchExitsDocumentation": "Відсоток відвідувань, коли відвідувач пішов з сайту після пошуку на вашому сайті саме цього ключового слова.", + "ColumnSearchResultsCount": "Лічильник результатів", + "ColumnSearches": "Пошуки", + "ColumnSearchesDocumentation": "Число відвідувань, коли відвідувач шукав саме це ключове слово через пошук на сайті.", + "ColumnSiteSearchKeywords": "Унікальні ключові слова", + "ColumnUniqueClicks": "Унікальні кліки", + "ColumnUniqueClicksDocumentation": "Кількість відвідувань, коли було відкрито це посилання. Якщо посилання була відкрито кілька разів протягом одного і того ж відвідування, це враховується лише раз.", + "ColumnUniqueDownloads": "Унікальні завантаження", + "ColumnUniqueOutlinks": "Унікальні вихідні посилання", + "DownloadsReportDocumentation": "У цьому звіті ви можете бачити файли, які завантажували ваші відвідувачі. %s За завантаження вважається клік на відповідне посилання. Однак чи закінчив відвідувач завантаження залишається невідомим. За завантаження вважається також відкриття картинок і інших розширень файлів.", + "DownloadsSubcategoryHelp1": "У цьому звіті ви можете побачити, які файли завантажили ваші відвідувачі.", + "DownloadsSubcategoryHelp2": "Matomo вважає завантаженням натискання на посилання для завантаження. Matomo не знає, чи було завантаження завершено, чи ні.", + "EntryPageTitles": "Заголовки сторінок входу", + "EntryPageTitlesReportDocumentation": "Цей звіт містить інформацію про заголовки вхідних сторінок, на які потрапляли відвідувачі в конкретний період.", + "EntryPagesReportDocumentation": "Цей звіт містить інформацію про сторінки входу, які використовувались протягом зазначеного періоду. Сторінка входу - це перша сторінка, яку користувач переглядає під час відвідування. %s Вхідні посилання відображаються як структура тек.", + "EntryPagesSubcategoryHelp1": "Цей звіт містить інформацію про сторінки входу, які використовувалися протягом зазначеного періоду. Сторінка входу – це перша сторінка, яку переглядає користувач під час відвідування.", + "EntryPagesSubcategoryHelp2": "URL-адреси входу показуються у вигляді структури тек.", + "ExitPageTitles": "Заголовки сторінок виходу", + "ExitPageTitlesReportDocumentation": "Цей звіт містить інформацію про заголовки вихідних сторінок, з яких відвідувач пішов з сайту в конкретний період.", + "ExitPagesReportDocumentation": "Цей звіт містить інформацію про сторінки виходу, які відбулися протягом зазначеного періоду. Сторінка виходу - це остання сторінка, яку користувач переглядає під час відвідування. %s URL-адреси виходу відображаються як структура тек.", + "ExitPagesSubcategoryHelp1": "Цей звіт містить інформацію про сторінки виходу, які відбулися протягом зазначеного періоду. Сторінка виходу – це остання сторінка, яку переглядає користувач під час відвідування.", + "ExitPagesSubcategoryHelp2": "URL-адреси виходу показуються у вигляді структури тек.", + "MainMetricsReportDocumentation": "Цей звіт надає базовий огляд того, які дії відвідувачі здійснюють на вашому вебсайті.", + "OneSearch": "1 пошук", + "OutlinkDocumentation": "Вихідне посилання – посилання, яка виводить відвідувача з вашого сайту (на інший домен).", + "OutlinksReportDocumentation": "Цей звіт показує ієрархічний список вихідних посилань, на які користувач натиснув.", + "OutlinksSubcategoryHelp1": "Цей звіт показує ієрархічний список URL-адрес зовнішніх посилань, на які переходили ваші відвідувачі. Вихідне посилання — це посилання, яке веде відвідувача з вашого сайту (на інший домен).", + "PageTitlesReportDocumentation": "Цей звіт містить інформацію про заголовки відвіданих сторінок. %1$s Заголовок сторінки це HTML-тег %2$s, який браузери відображають в заголовку вікна (вкладки).", + "PageTitlesSubcategoryHelp1": "Цей звіт містить інформацію про назви відвіданих сторінок.", + "PageTitlesSubcategoryHelp2": "Заголовок сторінки — це тег HTML &lt;title&gt;, який більшість браузерів показують у заголовку вікна.", + "PageUrls": "URL-и сторінок", + "PageViewsByVisitor": "Скільки разів ця сторінка була переглянута цим відвідувачем за всі відвідування", + "PagesReportDocumentation": "Цей звіт містить інформацію про адреси сторінок, на яких побували відвідувачі вашого сайту. %s Таблицю організовано ієрархічно – URL-и відображаються тековою структурою.", + "PagesSubcategoryHelp1": "Цей звіт містить інформацію про URL-адреси відвіданих сторінок.", + "PagesSubcategoryHelp2": "Таблиця організована ієрархічно, URL-адреси показано структурою тек.", + "PagesSubcategoryHelp3": "Для навігації використовуйте іконки плюс і мінус, що зліва.", + "PluginDescription": "Звіти про перегляди і заголовки сторінок. Дозволяють оцінити внутрішню пошукову систему сайту. Автоматично відстежує кліки по зовнішніх посиланнях і завантаженню файлів.", + "RevisitedPages": "Сторінки переглянуті більше одного разу", + "SiteSearchCategories": "Категорії (Пошук на сайті)", + "SiteSearchCategories1": "Цей звіт показує список категорій, які вибрали відвідувачі при пошуку на вашому сайті.", + "SiteSearchCategories2": "Наприклад, сайти електронної комерції зазвичай мають вибір «Категорій», щоб відвідувачам було зручно їх сортувати і шукати.", + "SiteSearchCategory": "Категорія (Пошук на сайті)", + "SiteSearchFollowingPagesDoc": "Коли відвідувачі шукають щось на вашому сайті, вони хочуть знайти певну сторінку, контент, продукт або послугу. Цей звіт відображає сторінки, за якими відвідувачі клацають найбільш часто при пошуку. Іншими словами, це список сторінок, які шукаються найчастіше на вашому сайті.", + "SiteSearchIntro": "Відстеження пошуку на вашому сайті – ефективний спосіб дізнатися, чим саме цікавиться ваша аудиторія. Це може допомогти знайти ідеї для нового контенту, нових продуктів і поліпшити взаємодію відвідувачів з вашим сайту в цілому.", + "SiteSearchKeyword": "Ключове слово (пошук по сайту)", + "SiteSearchKeywordCount": "Кількість ключових слів (пошук на сайті)", + "SiteSearchKeywordCounts": "Кількість ключових слів (пошук на сайті)", + "SiteSearchKeywords": "Ключове слово (Пошук на сайті)", + "SiteSearchKeywordsDocumentation": "Ці звіт показує список ключових слів, які відвідувачі шукали на вашому сайті через ваш пошук.", + "SiteSearchKeywordsNoResultDocumentation": "Цей звіт відображає список ключових слів, за якими не було нічого знайдено на вашому сайті: можливо, варто поліпшити пошуковий алгоритм на вашому сайті або відвідувачі шукають те, чого на вашому сайті немає?", + "SiteSearchSubcategoryHelp1": "Розділ «Пошук на сайті» показує, які ключові слова відвідувачі використовують під час пошуку на вашому вебсайті. Він також показує, які сторінки переглядають користувачі після виконання пошуку та які ключові слова пошуку на сайті взагалі не дають результатів.", + "SiteSearchSubcategoryHelp2": "Ці звіти можуть дати вам ідеї щодо відсутнього вмісту на вашому сайті, розуміння того, що ваші відвідувачі шукають, але не можуть легко знайти, і багато іншого.", + "SiteSearchSubcategoryHelp3": "Дізнайтеся більше в посібнику з пошуку на сайті.", + "SubmenuPageTitles": "Заголовки", + "SubmenuPagesEntry": "Сторінки входу", + "SubmenuPagesExit": "Сторінки виходу", + "SubmenuSitesearch": "Пошук по сайту", + "TimeSpentInReferringAction": "Час, витрачений на реферальну дію", + "WidgetEntryPageTitles": "Заголовки сторінок входу", + "WidgetExitPageTitles": "Заголовки сторінки виходу", + "WidgetPageTitles": "Заголовки сторінок", + "WidgetPageTitlesFollowingSearch": "Заголовки сторінок, які знайшлися в пошуку", + "WidgetPageUrlsFollowingSearch": "Сторінки, які знайшлися в пошуку", + "WidgetPagesEntry": "Сторінки входу", + "WidgetPagesExit": "Сторінки виходу", + "WidgetSearchCategories": "Категорії пошуку", + "WidgetSearchKeywords": "Пошукові ключові слова сайту", + "WidgetSearchNoResultKeywords": "Пошукові ключові слова без результатів" + } +} diff --git a/plugins/Actions/lang/ur.json b/plugins/Actions/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..58f071951728fab0c6aa394d6d446414f8110e11 --- /dev/null +++ b/plugins/Actions/lang/ur.json @@ -0,0 +1,38 @@ +{ + "Actions": { + "ColumnClicks": "کلکس", + "ColumnClicksDocumentation": "اس لنک پر کلک کی تعداد", + "ColumnDownloadURL": "ڈاوُنلوڈ یو آر ایل", + "ColumnEntryPageTitle": "داخلی صفحے کا عنوان", + "ColumnEntryPageURL": "داخلی صفحے کا یو آر ایل", + "ColumnExitPageTitle": "صفحہ عنوان سے باہر نکلیں", + "ColumnExitPageURL": "خارجی صفحے کا عنوان", + "ColumnNoResultKeyword": "کی ورڈ جنکی کوئی سرچ رزلٹ نہیں ہے", + "ColumnPageName": "صفحے کا نام", + "ColumnPageURL": "صفحے کا یو آر ایل", + "ColumnPagesPerSearch": "تلاش کے نتائج والے صفحات", + "ColumnPagesPerSearchDocumentation": "زائر بعض اوقات آپکی سائیٹ پر سرچ کرتا ہے اور کبھی \"\" پر کلک کرتا ہے مزید رزلٹس دیکھنے کے لئے۔ یہ اس کے ورڈ کیلئے دیکھے جانے والی نتائجی صفحات کی اوسط تعداد ہوتی ہے۔", + "ColumnSearchCategory": "تلاش کی اقسام", + "ColumnSearchExitsDocumentation": "اُن زائرین کی پرسنٹیج جنہوں نے آپکی سائیٹ کے سرچ انجن پر اس کی ورڈ کو سرچ کرنے کے بعد ویب سائیٹ کو چھوڑ دیا ۔", + "ColumnSearchResultsCount": "تلاش کے نتائج کی تعداد", + "ColumnSearches": "تلاشیں", + "ColumnSiteSearchKeywords": "منفرد مطلوبہ الفاظ", + "ColumnUniqueClicks": "یونیک کلکس کی تعداد", + "ColumnUniqueClicksDocumentation": "زائرین کی تعداد جنہوں نے اس لنک پر کلک کیا ہے۔ اگر ایک وزٹ کے دوران ایک لنک کئی بار کلک کیا گیا تھا تو اسے ایک مرتبہ ہی گنا جائے گا۔", + "ColumnUniqueOutlinks": "منفرد آوُٹ لنکس", + "DownloadsReportDocumentation": "اس رپورٹ میں، آپ دیکھ سکتے ہیں کہ زائرین نے کن فائلز کو ڈاؤنلوڈ کیا ہے۔ %s کسی ڈاؤنلوڈ لنک پر کلک کرنے کو Matomo ڈاؤنلوڈ شمار کرتا ہے۔ چاہے ڈاؤنلوڈ مکمل ہو یا نہ ہو۔ اسکا Matomo کو پتا نہیں چلتا۔", + "EntryPageTitles": "داخلی صفحوں کے عنوانات", + "EntryPageTitlesReportDocumentation": "اس رپورٹ میں اُن اینٹری صفحات کے عنوانات کی معلومات موجود ہیں جنکو مخصوص مدت کے دوران استعمال کیا گیا تھا۔", + "ExitPageTitles": "خارجی صفحوں کے عنوانات", + "OutlinksReportDocumentation": "یہ رپورٹ آوٹ لنک یو آر ایلز کی مرتب فہرست ظاہر کرتی ہے جو آپکے زائرین کی طرف سے کلک کئے گئے ہیں۔", + "PageUrls": "صفحے کا یو آر ایل", + "SiteSearchKeyword": "مطلوبہ لفظ (سائیٹ کی سرچ)", + "SubmenuPagesExit": "خارجی صفحات", + "SubmenuSitesearch": "سائیٹ سرچ", + "WidgetExitPageTitles": "خارجی صفحات کے عنوانات", + "WidgetPageTitlesFollowingSearch": "سائیٹ سرچ کے بعد صفحوں کے عنوانات", + "WidgetPagesExit": "خارجی صفحات", + "WidgetSearchCategories": "تلاش کے کیٹاگریز", + "WidgetSearchNoResultKeywords": "غیر نتائج والے سرچ کے الفاظ" + } +} diff --git a/plugins/Actions/lang/vi.json b/plugins/Actions/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..2bf788905bae86fdaefcb61c85a9582156c9d8bb --- /dev/null +++ b/plugins/Actions/lang/vi.json @@ -0,0 +1,63 @@ +{ + "Actions": { + "ActionType": "Loại hành động", + "AvgGenerationTimeTooltip": "Trung bình dựa trên %1$s lần %2$s giữa %3$s và %4$s", + "ColumnActionURL": "URL hành động", + "ColumnClicks": "Clicks", + "ColumnClicksDocumentation": "Số lần đường dẫn này đã được click", + "ColumnDownloadURL": "Đường dẫn tải về", + "ColumnEntryPageTitle": "Tiêu đề trang khởi đầu", + "ColumnEntryPageURL": "URL trang khởi đầu", + "ColumnExitPageTitle": "Tiêu đề trang rời đi", + "ColumnExitPageURL": "URL trang rời đi", + "ColumnNoResultKeyword": "Từ khóa không có kết quả tìm kiếm", + "ColumnPageName": "Tên trang", + "ColumnPageURL": "URL của trang", + "ColumnPagesPerSearch": "Kết quả tìm kiếm", + "ColumnPagesPerSearchDocumentation": "Khách truy cập sẽ tìm kiếm trên trang web của bạn, và đôi khi click \"Next\" để xem nhiều kết quả hơn. Đây là trung bình số trang kết quả đã tìm kiếm cho từ khóa này.", + "ColumnSearchCategory": "Danh mục tìm kiếm", + "ColumnSearchExitsDocumentation": "Tỷ lệ khách rời website sau khi tìm kiếm Từ khóa này trên công cụ tìm kiếm Site của bạn", + "ColumnSearchResultsCount": "Số kết quả tìm kiếm", + "ColumnSearches": "Tìm kiếm", + "ColumnSearchesDocumentation": "Số truy cập tìm kiếm cho từ khóa này bằng công cụ tìm kiếm trên website của bạn.", + "ColumnSiteSearchKeywords": "Từ khóa duy nhất", + "ColumnUniqueClicks": "Số click duy nhất", + "ColumnUniqueClicksDocumentation": "Số lần truy cập liên quan đến một click chuột vào liên kết này. Nếu một liên kết đã được click nhiều lần trong một lần truy cập, nó chỉ được tính một lần.", + "ColumnUniqueDownloads": "Lượt tải về duy nhất", + "ColumnUniqueOutlinks": "Đường link bên ngoài", + "DownloadsReportDocumentation": "Trong báo cáo này, bạn có thể thấy những file nào khách truy cập đã download. %s Cái gì mà Matomo tính như một tải về là một click trên một liên kết tải về. Cho dù tải về đã được hoàn thành hoặc không được biết đến Matomo.", + "EntryPageTitles": "Tiêu đề trang khởi đầu", + "EntryPageTitlesReportDocumentation": "Báo cáo này chứa thông tin về các tiêu đề của các trang đến đã được sử dụng trong một thời hạn quy định.", + "ExitPageTitles": "Tiêu đề trang rời đi", + "ExitPageTitlesReportDocumentation": "Báo cáo này có chứa thông tin về những tiêu đề của những trang thoát xuất hiện trong một khoảng thời gian nhất định.", + "OneSearch": "1 lần tìm kiếm", + "OutlinkDocumentation": "Một outlink là một liên kết dẫn người truy cập từ trang web của bạn (đến tên miền khác)", + "OutlinksReportDocumentation": "Báo cáo này cho thấy một danh sách phân cấp của outlink URL đã được click bởi khách truy cập của bạn", + "PageTitlesReportDocumentation": "Báo cáo này chứa thông tin về tiêu đề các trang đã được truy cập. %1$s Tiêu đề trang là thẻ HTML %2$s mà các trình duyệt hiển thị trên thanh tiêu đề của cửa sổ trình duyệt.", + "PageUrls": "Đường dẫn trang", + "PagesReportDocumentation": "Báo cáo này chứa thông tin về các URLs của trang đã được xem. %s Bảng này được tổ chức dưới dạng cây, Các URL được hiển thị dưới cấu trúc folder.", + "PagesSubcategoryHelp3": "Sử dụng các biểu tượng cộng và trừ ở bên trái để di chuyển.", + "PluginDescription": "Các báo cáo về lượt xem trang và tiêu đề trang. Cho phép bạn đo lường công cụ tìm kiếm của trang web nội bộ của bạn. Tự động theo dõi các lượt nhấp chuột vào liên kết bên ngoài và lượt tải về tập tin.", + "SiteSearchCategories1": "Báo cáo này liệt kê các hạng mục cho người truy cập lựa chọn khi họ thực hiện một tìm kiếm trên trang web của bạn", + "SiteSearchCategories2": "Ví dụ, một trang thương mại điện tử (e-commerce) thường có chức năng \"Lựa chọn Phân mục\" để khách truy cập giới hạn phạm vi tìm kiếm đối với các sản phẩm trong một phân mục quy định.", + "SiteSearchFollowingPagesDoc": "Khi khách tìm kiếm trên trang web của bạn, họ tìm kiếm một trang, nội dung, sản phẩm hoặc một dịch vụ cụ thể. Báo cáo này liệt kê các trang đã được click nhiều nhất sau khi thực hiện phép tìm kiếm (nội bộ). Nói cách khác, danh sách này chứa các trang được tìm kiếm nhiều nhất được thực hiện bởi khách trên trang web của bạn.", + "SiteSearchIntro": "Theo dõi các tìm kiếm mà khách đã thực hiện trên trang web của bạn là một cách hữu hiệu để hiểu hơn về thói quen tìm kiếm của khách hàng, và có thể giúp bạn tìm ra ý tưởng cho nội dung mới, sản phẩm thương mại điện tử mới mà những khách hàng tiềm năng có thể tìm kiếm, và cải thiện trải nghiệm của khách hàng trên trang web của bạn.", + "SiteSearchKeyword": "Từ khóa (trang tìm kiếm)", + "SiteSearchKeywordsDocumentation": "Báo cáo này liệt kê các từ khóa tìm kiếm mà người truy cập thực hiện tìm kiếm trên công cụ tìm kiếm nội bộ của bạn", + "SiteSearchKeywordsNoResultDocumentation": "Báo cáo này liệt kê các từ khóa tìm kiếm không trả lại bất cứ kết quả nào → Có thể cần cải thiện máy tìm kiếm, hoặc có thể do người dùng tìm kiếm thứ chưa có trên website?", + "SubmenuPageTitles": "Tiêu đề trang", + "SubmenuPagesEntry": "Trang khởi đầu", + "SubmenuPagesExit": "Trang rời đi", + "SubmenuSitesearch": "Trang tìm kiếm", + "WidgetEntryPageTitles": "Tiêu đề trang đầu vào", + "WidgetExitPageTitles": "Số tiêu đề trang thoát", + "WidgetPageTitles": "Tiêu đề trang", + "WidgetPageTitlesFollowingSearch": "Các trang ghé thăm sau trang tìm kiếm", + "WidgetPageUrlsFollowingSearch": "Các trang ghé thăm sau trang tìm kiếm", + "WidgetPagesEntry": "Trang khởi đầu", + "WidgetPagesExit": "Trang rời đi", + "WidgetSearchCategories": "Số danh mục tìm kiếm", + "WidgetSearchKeywords": "Số từ khóa trang tìm kiếm", + "WidgetSearchNoResultKeywords": "Các từ khóa tìm kiếm không có kết quả" + } +} diff --git a/plugins/Actions/lang/zh-cn.json b/plugins/Actions/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..ec67a74c838be88af17046f8e1fb7fdab3a0be39 --- /dev/null +++ b/plugins/Actions/lang/zh-cn.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "活动类型", + "ActionTypes": "活动类型", + "ActionsInVisit": "访问的活动次数", + "AvgGenerationTimeTooltip": "平均值基于 %1$s 次访问 %2$s,介于 %3$s 和 %4$s 之间", + "Behaviour": "行为", + "ColumnActionURL": "活动网址", + "ColumnClickedURL": "点击的外部链接", + "ColumnClickedURLs": "点击的链接", + "ColumnClicks": "点击次数", + "ColumnClicksDocumentation": "该链接被点击的次数。", + "ColumnDownloadURL": "下载网址", + "ColumnDownloadURLs": "下载的网址总数", + "ColumnEntryPageTitle": "进入页面标题", + "ColumnEntryPageURL": "进入页面网址", + "ColumnEntryPageURLs": "入口页面网址", + "ColumnExitPageTitle": "退出页面标题", + "ColumnExitPageURL": "退出页面网址", + "ColumnExitPageURLs": "出口页面网址", + "ColumnIdPageview": "页面浏览 ID", + "ColumnInteractionPosition": "互动位置", + "ColumnNoResultKeyword": "查无资料的关键词", + "ColumnPageName": "页面名称", + "ColumnPageURL": "页面网址", + "ColumnPageViewPosition": "浏览量排名", + "ColumnPagesPerSearch": "搜索结果页", + "ColumnPagesPerSearchDocumentation": "访客会在您的网站上搜索,有时会点击“下一页”来查看更多结果。这是此关键词搜索结果页面被访问的平均页数。", + "ColumnSearchCategory": "搜索类别", + "ColumnSearchExits": "%%搜索退出数", + "ColumnSearchExitsDocumentation": "访客使用站内搜索功能检索此关键词后即离开网站的百分比。", + "ColumnSearchResultsCount": "搜索结果数", + "ColumnSearches": "搜索", + "ColumnSearchesDocumentation": "使用站内搜索功能检索此关键词的次数。", + "ColumnSiteSearchKeywords": "不重复关键词", + "ColumnUniqueClicks": "不重复点击数", + "ColumnUniqueClicksDocumentation": "访客点击此链接的次数。如果链接在同一次访问期间被点击多次,只算作一次。", + "ColumnUniqueDownloads": "不重复下载次数", + "ColumnUniqueOutlinks": "不重复退出链接数", + "DownloadsReportDocumentation": "在这份报表中,您可以看到用户下载了那些文件。%s Matomo 计算的是下载链接的点击数量,至于文件是否下载完成 Matomo 无法知道。", + "DownloadsSubcategoryHelp1": "在此报告中,您可以看到访问者下载了哪些文件。", + "DownloadsSubcategoryHelp2": "Matomo 计算的是下载链接的点击数量;Matomo 并不知道下载是否完成。", + "EntryPageTitles": "进入页面标题", + "EntryPageTitlesReportDocumentation": "此报表包含指定时间段内的进入页面的标题信息。", + "EntryPagesReportDocumentation": "此报表包含在指定时间段内的进入页面的相关信息。进入页面是指用户进入网站时访问的第一个页面。%s进入页面的网址以文件夹结构展示。", + "EntryPagesSubcategoryHelp1": "此报告包含有关在指定时间段内使用的条目页面的信息。进入页面是用户在访问期间查看的第一个页面。", + "EntryPagesSubcategoryHelp2": "条目URL以文件夹结构的形式显示。", + "ExitPageTitles": "退出页面标题", + "ExitPageTitlesReportDocumentation": "此报表包含指定时间段内的退出页面的标题信息。", + "ExitPagesReportDocumentation": "此报表包含指定时间段内的退出页面的信息。退出页面是指用户离开网站前访问的最后一个页面。%s退出页面的网址以文件夹结构展示。", + "ExitPagesSubcategoryHelp1": "此报表包含有关在指定期间发生的退出页面的信息。退出页面是用户在访问期间查看的最后一个页面。", + "ExitPagesSubcategoryHelp2": "退出的URL显示为一个文件夹结构。", + "MainMetricsReportDocumentation": "本报告提供了访问者在网站上采取的行动的基本概述。", + "OneSearch": "一个搜索", + "OutlinkDocumentation": "离站链接指将访客从您的网站带出到其他域名的链接。", + "OutlinksReportDocumentation": "此报表以分级列表方式显示访客点击的离站链接网址。", + "OutlinksSubcategoryHelp1": "此报表显示访问者单击外链 URL 的列表。外链是从您的网站指向另一个网站的链接。", + "PageTitlesReportDocumentation": "此报表包含被访问页面的标题信息。%1$s 网页标题是 HTML 标签 %2$s 中的内容,在大部分浏览器上都会显示为窗口的标题。", + "PageTitlesSubcategoryHelp1": "此报告包含有关已访问页面标题的信息。", + "PageTitlesSubcategoryHelp2": "页面标题是HTML&lt;title&gt;标签,大多数浏览器在其窗口标题中显示。", + "PageUrls": "网页网址", + "PageViewsByVisitor": "当前访客所有访问中此网页的浏览次数", + "PagesReportDocumentation": "此报表显示被访问的网页地址信息。%s 表格以分级显示,网址以目录结构显示。", + "PagesSubcategoryHelp1": "此报表包含有关已访问的页面URL的信息。", + "PagesSubcategoryHelp2": "该表按层次结构组织,URL显示为文件夹结构。", + "PagesSubcategoryHelp3": "点击左边的 + 、 - 图标可展开\\/合并分类。", + "PluginDescription": "有关页面访问量和页面标题的报告。让您衡量您的内部网站的搜索引擎。自动跟踪外部链接及文件下载链接的点击。", + "RevisitedPages": "被浏览超过一次的页面", + "SiteSearchCategories": "所有分类(站内搜索)", + "SiteSearchCategories1": "此报表显示访客使用站内搜索时选择的分类。", + "SiteSearchCategories2": "例如,电子商务网站通常有“分类”选项,以便访客可以限制在某个分类中搜索产品。", + "SiteSearchCategory": "分类(站内搜索)", + "SiteSearchFollowingPagesDoc": "当访客在您的网站上搜索时,他们是在寻找特定的页面、内容、产品或者服务。此报表显示站内搜索后点击最多的页面。也就是说,是一个在您的网站上最常被访客搜索并前往的页面列表。", + "SiteSearchIntro": "追踪访客在您网站上的搜索,是一个了解用户在寻找什么的有效方法。它有助于发现潜在用户可能在搜寻的新内容、新商品的点子,并从整体上改进访客在您网站的体验。", + "SiteSearchKeyword": "关键词(站内搜索)", + "SiteSearchKeywordCount": "关键词计数(网站搜索)", + "SiteSearchKeywordCounts": "关键词计数(网站搜索)", + "SiteSearchKeywords": "关键词(站内搜索)", + "SiteSearchKeywordsDocumentation": "此报表显示访客使用站内搜索时用到的搜索关键词。", + "SiteSearchKeywordsNoResultDocumentation": "此报表列出没有任何搜索结果的关键词:或许搜索算法需要改进,或是访客要找的内容在您的网站上还没有?", + "SiteSearchSubcategoryHelp1": "网站搜索部分显示访问者在搜索网站内容的搜索词。它还显示用户在执行搜索后查看的页面,以及哪些搜索关键词没有返回结果。", + "SiteSearchSubcategoryHelp2": "这些报告可以让你了解网站上缺失的内容,了解访问者正在寻找但不容易找到的内容等等。", + "SiteSearchSubcategoryHelp3": "在站点搜索指南中了解更多信息。", + "SubmenuPageTitles": "页面标题", + "SubmenuPagesEntry": "进入页面", + "SubmenuPagesExit": "退出页面", + "SubmenuSitesearch": "站内搜索", + "TimeSpentInReferringAction": "引用操作所花费的时间", + "WidgetEntryPageTitles": "进入页面标题", + "WidgetExitPageTitles": "退出页面标题", + "WidgetPageTitles": "页面标题", + "WidgetPageTitlesFollowingSearch": "从站内搜索前往的页面标题", + "WidgetPageUrlsFollowingSearch": "从站内搜索前往的页面", + "WidgetPagesEntry": "进入页面", + "WidgetPagesExit": "退出页面", + "WidgetSearchCategories": "搜索分类", + "WidgetSearchKeywords": "站内搜索关键词", + "WidgetSearchNoResultKeywords": "查无资料的关键词" + } +} diff --git a/plugins/Actions/lang/zh-tw.json b/plugins/Actions/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..ae1789a5745a1af16a493f097a52bb22dccf10a8 --- /dev/null +++ b/plugins/Actions/lang/zh-tw.json @@ -0,0 +1,100 @@ +{ + "Actions": { + "ActionType": "活動類型", + "ActionTypes": "活動類型", + "ActionsInVisit": "訪問中的活動數", + "AvgGenerationTimeTooltip": "基於 %1$s 次點擊%2$s介於 %3$s 到 %4$s", + "Behaviour": "行為", + "ColumnActionURL": "活動網址", + "ColumnClickedURL": "點擊的離開連結", + "ColumnClickedURLs": "點擊的離開連結", + "ColumnClicks": "點擊數", + "ColumnClicksDocumentation": "此連結被點擊的次數。", + "ColumnDownloadURL": "下載網址", + "ColumnDownloadURLs": "下載網址", + "ColumnEntryPageTitle": "到達網頁標題", + "ColumnEntryPageURL": "到達網頁網址", + "ColumnEntryPageURLs": "到達網頁網址", + "ColumnExitPageTitle": "離站網頁標題", + "ColumnExitPageURL": "離站網頁網址", + "ColumnExitPageURLs": "離站網頁網址", + "ColumnIdPageview": "瀏覽數 ID", + "ColumnInteractionPosition": "互動位置", + "ColumnNoResultKeyword": "查無資料的關鍵字", + "ColumnPageName": "網頁名稱", + "ColumnPageURL": "網頁網址", + "ColumnPageViewPosition": "瀏覽數位置", + "ColumnPagesPerSearch": "搜尋結果", + "ColumnPagesPerSearchDocumentation": "訪客在你的網站中搜尋,有些時候會點擊「下一頁」來查看更多結果。這是這個關鍵字的搜尋結果頁面瀏覽次數。", + "ColumnSearchCategory": "搜索類別", + "ColumnSearchExits": "%% 搜尋離站數", + "ColumnSearchExitsDocumentation": "訪客在你的網站的搜尋引擎搜尋此關鍵字後即離開網站的百分比。", + "ColumnSearchResultsCount": "搜尋結果筆數", + "ColumnSearches": "搜尋數", + "ColumnSearchesDocumentation": "訪客在你網站搜尋引擎所搜尋的關鍵字次數。", + "ColumnSiteSearchKeywords": "不重複關鍵字數", + "ColumnUniqueClicks": "不重複點擊數", + "ColumnUniqueClicksDocumentation": "訪客點擊這個連結的次數。如果連結在同一次訪問中被點擊多次,只會計算成一次。", + "ColumnUniqueDownloads": "不重複下載數", + "ColumnUniqueOutlinks": "不重複離開連結數", + "DownloadsReportDocumentation": "在這份報表你可以查看訪客已下載了哪個檔案。%sMatomo 是透過點擊下載連結來計算下載數,但不知道下載完成與否。", + "DownloadsSubcategoryHelp1": "在此报告中你可以看到访问者下载了哪些文件。", + "DownloadsSubcategoryHelp2": "Matomo 將點擊下載連結計作 1 次下載,但 Matomo 並不知道下載是否已完成。", + "EntryPageTitles": "到達網頁標題", + "EntryPageTitlesReportDocumentation": "此報告包含指定期間內,到達網頁標題的相關資訊。", + "EntryPagesReportDocumentation": "此報表包含指定期間內之到達網頁的相關資訊。所謂的到達網頁,是訪客進入網站時所訪問的第一個網頁。%s 到達網頁的網址以資料夾結構顯示。", + "EntryPagesSubcategoryHelp1": "此報表包括在指定期間內所用的入口頁。入口頁是使用者在他們的訪問中第一個頁面。", + "EntryPagesSubcategoryHelp2": "入口 URL 顯示為資料夾結構。", + "ExitPageTitles": "離站網頁標題", + "ExitPageTitlesReportDocumentation": "此報表包含關於指定期間內離站網頁之標題等資訊。", + "ExitPagesReportDocumentation": "這份報表包含指定期間內之離站頁的相關資訊。離站頁是訪客離開網頁前最後瀏覽的頁面。%s 離站頁的網址以資料夾結構顯示。", + "ExitPagesSubcategoryHelp1": "此報表包括在指定期間內發生的離站頁。離站頁是使用者在他們的訪問中最後一個頁面。", + "ExitPagesSubcategoryHelp2": "離站 URL 會顯示為資料夾結構。", + "MainMetricsReportDocumentation": "這份報表提供了訪客在你的網站上的基本活動概觀。", + "OneSearch": "1 搜尋數", + "OutlinkDocumentation": "離開連結是將訪客帶出網站的連結(到其他域名)。", + "OutlinksReportDocumentation": "這份報表以分層列表顯示訪客所點擊的離開連結網址。", + "OutlinksSubcategoryHelp1": "此報表顯示訪客點擊外部連結 URL 的紀錄。外部連結,即在您網站上,引導使用者從您的網站轉向其他(域的)網站的連結。", + "PageTitlesReportDocumentation": "這份報表包含瀏覽過的網頁標題資訊。%1$s網頁標題是在 HTML 標籤 %2$s 中的內容,也是多數瀏覽器上顯示的標題內容。", + "PageTitlesSubcategoryHelp1": "此報表包括被訪問頁面的標題資訊。", + "PageTitlesSubcategoryHelp2": "網頁標題即為 HTML &lt;title&gt; 標籤,大多數瀏覽器會顯示在窗口標題位置。", + "PageUrls": "網頁網址", + "PageViewsByVisitor": "這個訪客所有訪問紀錄中瀏覽此網頁的次數", + "PagesReportDocumentation": "這份報表包含瀏覽過的網頁網址資訊。%s表格以分層顯示,網址以資料夾結構顯示。", + "PagesSubcategoryHelp1": "此報表包含被訪問網頁 URL 相關資訊。", + "PagesSubcategoryHelp2": "表格按層次組織,URL 顯示為資料夾結構。", + "PagesSubcategoryHelp3": "使用左邊的 +、- 圖示來導航。", + "PluginDescription": "關於網頁瀏覽量和網頁標題的報表。讓您衡量內部網站的搜尋引擎。自動追蹤外部連結和檔案下載的次數。", + "RevisitedPages": "網頁瀏覽數超過一次", + "SiteSearchCategories": "分類(站內搜尋)", + "SiteSearchCategories1": "這份報表列出訪客在站內搜尋時所選擇的分類。", + "SiteSearchCategories2": "例如,電子商務網站通常會有「分類」選項,訪客就可以將他們的搜尋結果限制於所有產品或是特定分類。", + "SiteSearchCategory": "分類(站內搜尋)", + "SiteSearchFollowingPagesDoc": "當訪客在你的網站上搜尋時,他們會尋找特定的網頁、內容、產品或服務。這份報表列出站內搜尋最常被點擊的網頁連結。換句話說,是一個在你網站上最常被訪客搜尋並前往的網頁網址列表。", + "SiteSearchIntro": "追蹤訪客在你網站上的搜尋是一個了解訪客要尋找什麼的有效方法。它可以幫助你找到新內容的點子、淺在客戶可能會尋找的新電子商務產品,並增進訪客在你網站上的體驗。", + "SiteSearchKeyword": "關鍵字(站內搜尋)", + "SiteSearchKeywordCount": "關鍵字數量(站內搜尋)", + "SiteSearchKeywordCounts": "關鍵字數量(站內搜尋)", + "SiteSearchKeywords": "關鍵字(站內搜尋)", + "SiteSearchKeywordsDocumentation": "這份報表列出訪客在站內搜尋時所使用的搜尋關鍵字。", + "SiteSearchKeywordsNoResultDocumentation": "這份報表列出沒有任何搜尋結果的關鍵字:或許搜尋演算法可以改進,或是訪客要尋找的東西還不存在於你的網站?", + "SiteSearchSubcategoryHelp1": "站內搜尋顯示訪客使用甚麼關鍵字搜尋您的網站。同時也顯示使用者在進行搜尋後瀏覽了哪個網頁以及哪些站內搜尋關鍵字並沒有返回任何結果。", + "SiteSearchSubcategoryHelp2": "這些報表能讓您了解網站缺失的內容,洞察訪客正找尋但不容易找到的內容等等。", + "SiteSearchSubcategoryHelp3": "访问站点搜索指南了解更多。", + "SubmenuPageTitles": "網頁標題", + "SubmenuPagesEntry": "到達網頁", + "SubmenuPagesExit": "離站網頁", + "SubmenuSitesearch": "站內搜尋", + "TimeSpentInReferringAction": "參照連結動作花費的時間", + "WidgetEntryPageTitles": "到達網頁標題", + "WidgetExitPageTitles": "離站網頁標題", + "WidgetPageTitles": "網頁標題", + "WidgetPageTitlesFollowingSearch": "從站內搜尋前往的網頁標題", + "WidgetPageUrlsFollowingSearch": "從站內搜尋前往的網頁", + "WidgetPagesEntry": "到達網頁", + "WidgetPagesExit": "離站網頁", + "WidgetSearchCategories": "搜尋分類", + "WidgetSearchKeywords": "站內搜尋關鍵字", + "WidgetSearchNoResultKeywords": "查無資料的關鍵字" + } +} diff --git a/plugins/Actions/templates/_profileSummary.twig b/plugins/Actions/templates/_profileSummary.twig new file mode 100644 index 0000000000000000000000000000000000000000..3007a937a18f525be37d8af28f75e6048e0cec44 --- /dev/null +++ b/plugins/Actions/templates/_profileSummary.twig @@ -0,0 +1,25 @@ +{% if visitorData.totalPageViews > 0 %} + <div class="visitor-profile-summary visitor-profile-pages"> + <h1>{{ 'Actions_RevisitedPages'|translate }}</h1> + <div> + <ul> + {% for url,count in visitorData.visitedPages %} + {% if loop.index < 6 and count > 1 %} + <li><span class="repeat icon-refresh" title="{{ 'Actions_PageViewsByVisitor'|translate }}">{{ count }}</span> + {% if url|trim|lower starts with 'javascript:' or + url|trim|lower starts with 'vbscript:' or + url|trim|lower starts with 'data:' %} + {{ url }} + {% else %} + <a href="{{ url|safelink|e('html_attr') }}" rel="noreferrer noopener" target="_blank" + class="action-list-url truncated-text-line"> + {{ url|replace({'http://': '', 'https://': ''}) }} + </a> + {% endif %} + </li> + {% endif %} + {% endfor %} + </ul> + </div> + </div> +{% endif %} diff --git a/plugins/Actions/tests/Integration/ActionSiteSearchTest.php b/plugins/Actions/tests/Integration/ActionSiteSearchTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ee0c03390ad6833e8c06ddb9313deabf81b9b48f --- /dev/null +++ b/plugins/Actions/tests/Integration/ActionSiteSearchTest.php @@ -0,0 +1,67 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\tests\Unit; + +use Piwik\Plugins\Actions\Actions\ActionSiteSearch; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; +use Piwik\Tracker\Request; + +/** + * @group Actions + * @group SiteSearch + * @group Plugins + */ +class ActionSiteSearchTest extends IntegrationTestCase +{ + public function setUp(): void + { + parent::setUp(); + + Fixture::createWebsite('2010-01-01'); + } + + /** + * @dataProvider getSiteSearchData + */ + public function testSiteSearchDetection($website, $url, $expectedResult) + { + $action = new ActionSiteSearch(new Request(['idsite' => 1])); + $this->assertEquals($expectedResult, ActionSiteSearch::detectSiteSearchFromUrl($website, parse_url($url))); + } + + public function getSiteSearchData() + { + $defaultWebsite = [ + 'idSite' => 1, + 'sitesearch_keyword_parameters' => ['q','k'], + 'sitesearch_category_parameters' => ['cat','cc'], + ]; + + return [ + [$defaultWebsite, 'http://example.org/index.htm?q=keyword', ['http://example.org/index.htm', 'keyword', '', false]], + [$defaultWebsite, 'http://example.org/index.htm#q=keyword&cat=test', ['http://example.org/index.htm', 'keyword', 'test', false]], + [$defaultWebsite, 'http://example.org/index.htm#&q=keyword', ['http://example.org/index.htm', 'keyword', '', false]], + [$defaultWebsite, 'http://example.org/index.htm#?cat=test&q=keyword', ['http://example.org/index.htm', 'keyword', 'test', false]], + [$defaultWebsite, 'http://example.org/index.htm#?cat=test&q=keyword&otherparam=1', ['http://example.org/index.htm#otherparam=1', 'keyword', 'test', false]], + [$defaultWebsite, 'http://example.org/index.htm?cat=test&otherparam=1&q=keyword', ['http://example.org/index.htm?otherparam=1', 'keyword', 'test', false]], + [$defaultWebsite, 'http://example.org/index.htm#anchor?cat=test&otherparam=1&q=keyword', ['http://example.org/index.htm#anchor?otherparam=1', 'keyword', 'test', false]], + [$defaultWebsite, 'http://example.org/index.htm?cat=test&otherparam=1&q=kw#?q=keyword', ['http://example.org/index.htm?otherparam=1', 'keyword', 'test', false]], + [$defaultWebsite, 'http://example.org/index.htm?k=keyword&cc=cat', ['http://example.org/index.htm', 'keyword', 'cat', false]], + [$defaultWebsite, '#?q=keyword', ['', 'keyword', '', false]], + [$defaultWebsite, 'http://example.org/index.html?a=b#?&&&q=keyword', ['http://example.org/index.html?a=b', 'keyword', '', false]], + [$defaultWebsite, 'http://example.org/#&?q=keyword', ['http://example.org/#&', 'keyword', '', false]], + + // some invalid/incorrect urls that aren't detected as site search + [$defaultWebsite, 'http://example.org/index.html?a=b#?&&&?q=keyword', false], + [$defaultWebsite, 'http://example.org/#&?var=val?q=keyword', false], + ]; + } +} diff --git a/plugins/Actions/tests/System/ApiInvalidParameterTypeTest.php b/plugins/Actions/tests/System/ApiInvalidParameterTypeTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c133a5ef7acb4135b0bdb507f7d2e9b71b2ce2f6 --- /dev/null +++ b/plugins/Actions/tests/System/ApiInvalidParameterTypeTest.php @@ -0,0 +1,79 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\tests\System; + +use Piwik\API\Request; +use Piwik\Archive; +use Piwik\DataTable; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group ApiInvalidParameterTypeTest + */ +class ApiInvalidParameterTypeTest extends IntegrationTestCase +{ + public function testActionUrlSegmentValueIsProperlyEncodedInActionsReports() + { + $url = 'http://example+site.org/a+b/index.html'; + + $idSite = Fixture::createWebsite('2012-03-04 00:00:00'); + $t = Fixture::getTracker($idSite, '2015-03-04 03:24:00'); + $t->setUrl($url); + Fixture::checkResponse($t->doTrackPageView('a page+view')); + + // Attempt to call an API method with a string idSubtable value + try { + + /** @var DataTable $urls */ + $urls = Request::processRequest('Actions.getPageUrls', [ + 'idSite' => $idSite, + 'idSubtable' => 'undefined', // This is invalid + 'period' => 'day', + 'date' => '2015-03-04', + 'flat' => '1', + ]); + + $this->fail('Exception was not thrown'); + } catch (\Throwable $e) { + $this->assertStringStartsWith('idSubtable needs to be a number', $e->getMessage()); + } + + // Attempt to call the same API method with a numeric idSubtable value + /** @var DataTable $urls */ + $urls = Request::processRequest('Actions.getPageUrls', [ + 'idSite' => $idSite, + 'idSubtable' => 1, // valid + 'period' => 'day', + 'date' => '2015-03-04', + 'flat' => '1', + ]); + + $this->assertEquals(1, $urls->getRowsCount()); + + // Attempt to call the same API method with the 'all' idSubtable value + /** @var DataTable $urls */ + $urls = Request::processRequest('Actions.getPageUrls', [ + 'idSite' => $idSite, + 'idSubtable' => Archive::ID_SUBTABLE_LOAD_ALL_SUBTABLES, // valid + 'period' => 'day', + 'date' => '2015-03-04', + 'flat' => '1', + ]); + + $this->assertEquals(1, $urls->getRowsCount()); + } + + protected static function configureFixture($fixture) + { + parent::configureFixture($fixture); + $fixture->createSuperUser = true; + } +} diff --git a/plugins/Actions/tests/System/ApiTest.php b/plugins/Actions/tests/System/ApiTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f97ff9fef6b01867369c76c77792f448173c6fae --- /dev/null +++ b/plugins/Actions/tests/System/ApiTest.php @@ -0,0 +1,86 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\tests\System; + +use Piwik\API\Request; +use Piwik\DataTable; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +class ApiTest extends IntegrationTestCase +{ + public function testActionUrlSegmentValueIsProperlyEncodedInActionsReports() + { + $url = 'http://example+site.org/a+b/index.html'; + + $idSite = Fixture::createWebsite('2012-03-04 00:00:00'); + $t = Fixture::getTracker($idSite, '2015-03-04 03:24:00'); + $t->setUrl($url); + Fixture::checkResponse($t->doTrackPageView('a page+view')); + + /** @var DataTable $urls */ + $urls = Request::processRequest('Actions.getPageUrls', [ + 'idSite' => $idSite, + 'period' => 'day', + 'date' => '2015-03-04', + 'flat' => '1', + ]); + + $this->assertEquals(1, $urls->getRowsCount()); + + $urlSegment = $urls->getFirstRow()->getMetadata('segment'); + + /** @var DataTable $urlsWithSegment */ + $urlsWithSegment = Request::processRequest('Actions.getPageUrls', [ + 'idSite' => $idSite, + 'period' => 'day', + 'date' => '2015-03-04', + 'segment' => $urlSegment, + 'flat' => '1', + ]); + + $this->assertEquals(1, $urlsWithSegment->getRowsCount()); + + // NOTE: the label here is incorrect due to SafeDecodeLabel. this is a known issue, but changing it would + // break BC elsewhere + $this->assertEquals('/a b/index.html', $urlsWithSegment->getFirstRow()->getColumn('label')); + + $pages = Request::processRequest('Actions.getPageTitles', [ + 'idSite' => $idSite, + 'period' => 'day', + 'date' => '2015-03-04', + 'flat' => '1', + ]); + + $this->assertEquals(1, $pages->getRowsCount()); + + $pageSegment = $pages->getFirstRow()->getMetadata('segment'); + + /** @var DataTable $pagesWithSegment */ + $pagesWithSegment = Request::processRequest('Actions.getPageTitles', [ + 'idSite' => $idSite, + 'period' => 'day', + 'date' => '2015-03-04', + 'segment' => $pageSegment, + 'flat' => '1', + ]); + $this->assertEquals(1, $pagesWithSegment->getRowsCount()); + + // NOTE: the label here is incorrect due to SafeDecodeLabel. this is a known issue, but changing it would + // break BC elsewhere + $this->assertEquals('a page view', $pagesWithSegment->getFirstRow()->getColumn('label')); + } + + protected static function configureFixture($fixture) + { + parent::configureFixture($fixture); + $fixture->createSuperUser = true; + } +} diff --git a/plugins/Actions/tests/System/MixedArchivingAggregationTest.php b/plugins/Actions/tests/System/MixedArchivingAggregationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cc3e539f87bef93f1f282ff0aaf0ac2d2e2f4643 --- /dev/null +++ b/plugins/Actions/tests/System/MixedArchivingAggregationTest.php @@ -0,0 +1,361 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\tests\System; + +use Piwik\Archive; +use Piwik\Common; +use Piwik\Config; +use Piwik\DataTable; +use Piwik\Db; +use Piwik\Metrics; +use Piwik\Plugins\Actions\Archiver; +use Piwik\Plugins\Actions\ArchivingHelper; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group Actions + * @group Plugins + */ +class MixedArchivingAggregationTest extends IntegrationTestCase +{ + public function testWeekAggregationCombinesLegacyAndFlatDaysAndMergesOthers() + { + $idSite = Fixture::createWebsite('2026-01-01 00:00:00'); + + $this->trackUrlHits($idSite, '2026-01-06', [ + '/a' => 9, + '/b' => 6, + '/c' => 3, + ]); + $this->trackUrlHits($idSite, '2026-01-07', [ + '/a' => 5, + '/b' => 2, + '/d' => 4, + ]); + + $config = Config::getInstance(); + $configKeys = [ + 'datatable_archiving_maximum_rows_actions_flat', + 'datatable_archiving_maximum_rows_actions', + 'datatable_archiving_maximum_rows_subtable_actions', + 'archiving_ranking_query_row_limit', + ]; + $configBackup = $this->backupGeneralConfig($configKeys); + + try { + // phase 1: archive legacy day (hierarchical only) with subtable truncation + $config->General['datatable_archiving_maximum_rows_actions_flat'] = 0; + $config->General['datatable_archiving_maximum_rows_actions'] = 1; + $config->General['datatable_archiving_maximum_rows_subtable_actions'] = 2; + $config->General['archiving_ranking_query_row_limit'] = 1; + $legacyDayArchive = Archive::build($idSite, 'day', '2026-01-06'); + $legacyDayHierarchical = $legacyDayArchive->getDataTable(Archiver::PAGE_URLS_RECORD_NAME); + $legacyDaySummaryHits = $this->sumSummaryHitsRecursively($legacyDayHierarchical); + + // phase 2: archive flat day with low ranking query limit to force day-level "Others" + $config->General['datatable_archiving_maximum_rows_actions_flat'] = 50; + $config->General['archiving_ranking_query_row_limit'] = 1; + $flatDayArchive = Archive::build($idSite, 'day', '2026-01-07'); + $flatDayArchive->getDataTable(Archiver::PAGE_URLS_RECORD_NAME); + $flatDayFlat = $flatDayArchive->getDataTable(Archiver::PAGE_URLS_FLAT_RECORD_NAME); + $flatDaySummaryHits = $this->getSummaryHits($flatDayFlat); + + // phase 3: aggregate week with higher flat limit to avoid additional week truncation + $config->General['datatable_archiving_maximum_rows_actions_flat'] = 50; + $config->General['archiving_ranking_query_row_limit'] = 100000; + $archive = Archive::build($idSite, 'week', '2026-01-07'); + $weekFlat = $archive->getDataTable(Archiver::PAGE_URLS_FLAT_RECORD_NAME); + $weekHierarchical = $archive->getDataTable(Archiver::PAGE_URLS_RECORD_NAME); + } finally { + $this->restoreGeneralConfig($configBackup); + } + + $this->assertDayArchiveRecordNames( + $idSite, + '2026-01-06', + [Archiver::PAGE_URLS_RECORD_NAME], + [Archiver::PAGE_URLS_FLAT_RECORD_NAME] + ); + $this->assertDayArchiveRecordNames( + $idSite, + '2026-01-07', + [Archiver::PAGE_URLS_RECORD_NAME, Archiver::PAGE_URLS_FLAT_RECORD_NAME], + [] + ); + + $this->assertGreaterThan(0, $legacyDaySummaryHits); + $this->assertGreaterThan(0, $flatDaySummaryHits); + + $weekSummaryRow = $weekFlat->getRowFromId(DataTable::ID_SUMMARY_ROW); + $this->assertNotFalse($weekSummaryRow); + $weekSummaryHits = (int) $weekSummaryRow->getColumn(Metrics::INDEX_PAGE_NB_HITS); + $this->assertSame($legacyDaySummaryHits + $flatDaySummaryHits, $weekSummaryHits); + + $nonSummaryHits = $this->sumHitsWithoutSummary($weekFlat); + $this->assertSame(29, $nonSummaryHits + $weekSummaryHits); + + $weekHierarchicalSummary = $weekHierarchical->getRowFromId(DataTable::ID_SUMMARY_ROW); + $this->assertNotFalse($weekHierarchicalSummary); + $this->assertSame($weekSummaryHits, (int) $weekHierarchicalSummary->getColumn(Metrics::INDEX_PAGE_NB_HITS)); + } + + public function testWeekAggregationParityBetweenLegacyAndFlatFirst() + { + $legacySiteId = Fixture::createWebsite('2026-02-01 00:00:00'); + $flatSiteId = Fixture::createWebsite('2026-02-01 00:00:00'); + + $hitsByDay = [ + '2026-02-02' => [ + '/shop/shoes/nike' => 5, + '/shop/shoes/adidas' => 3, + '/about' => 2, + ], + '2026-02-03' => [ + '/shop/shoes/nike' => 4, + '/blog/article-1' => 3, + '/contact' => 1, + ], + ]; + + foreach ($hitsByDay as $day => $urlHits) { + $this->trackUrlHits($legacySiteId, $day, $urlHits); + $this->trackUrlHits($flatSiteId, $day, $urlHits); + } + + $config = Config::getInstance(); + $configKeys = [ + 'datatable_archiving_maximum_rows_actions_flat', + 'datatable_archiving_maximum_rows_actions', + 'datatable_archiving_maximum_rows_subtable_actions', + 'archiving_ranking_query_row_limit', + ]; + $configBackup = $this->backupGeneralConfig($configKeys); + + try { + $config->General['datatable_archiving_maximum_rows_actions'] = 50000; + $config->General['datatable_archiving_maximum_rows_subtable_actions'] = 50000; + $config->General['archiving_ranking_query_row_limit'] = 100000; + + $config->General['datatable_archiving_maximum_rows_actions_flat'] = 0; + $legacyWeekArchive = Archive::build($legacySiteId, 'week', '2026-02-03'); + $legacyWeekUrls = $legacyWeekArchive->getDataTable(Archiver::PAGE_URLS_RECORD_NAME); + $legacyWeekTitles = $legacyWeekArchive->getDataTable(Archiver::PAGE_TITLES_RECORD_NAME); + + $config->General['datatable_archiving_maximum_rows_actions_flat'] = 50000; + $flatWeekArchive = Archive::build($flatSiteId, 'week', '2026-02-03'); + $flatWeekUrls = $flatWeekArchive->getDataTable(Archiver::PAGE_URLS_RECORD_NAME); + $flatWeekTitles = $flatWeekArchive->getDataTable(Archiver::PAGE_TITLES_RECORD_NAME); + } finally { + $this->restoreGeneralConfig($configBackup); + } + + $this->assertSame( + $this->exportHierarchyTableValues($legacyWeekUrls), + $this->exportHierarchyTableValues($flatWeekUrls) + ); + $this->assertSame( + $this->exportHierarchyTableValues($legacyWeekTitles), + $this->exportHierarchyTableValues($flatWeekTitles) + ); + } + + public function testDayFlatAndHierarchyStayInParityWhenHostsDifferButPathsMatch() + { + $idSite = Fixture::createWebsite('2026-02-10 00:00:00'); + + $tracker = Fixture::getTracker($idSite, '2026-02-10 00:00:01'); + $tracker->setUrl('http://EXAMPLE.org/shared/path#'); + Fixture::checkResponse($tracker->doTrackPageView('first')); + + $tracker->setForceVisitDateTime('2026-02-10 00:00:02'); + $tracker->setUrl('https://other.example/shared/path'); + Fixture::checkResponse($tracker->doTrackPageView('second')); + + $config = Config::getInstance(); + $configKeys = [ + 'datatable_archiving_maximum_rows_actions_flat', + 'datatable_archiving_maximum_rows_actions', + 'datatable_archiving_maximum_rows_subtable_actions', + 'archiving_ranking_query_row_limit', + ]; + $configBackup = $this->backupGeneralConfig($configKeys); + + try { + $config->General['datatable_archiving_maximum_rows_actions_flat'] = 50; + $config->General['datatable_archiving_maximum_rows_actions'] = 50000; + $config->General['datatable_archiving_maximum_rows_subtable_actions'] = 50000; + $config->General['archiving_ranking_query_row_limit'] = 100000; + + $archive = Archive::build($idSite, 'day', '2026-02-10'); + $flatUrls = $archive->getDataTable(Archiver::PAGE_URLS_FLAT_RECORD_NAME); + $hierarchicalUrls = $archive->getDataTable(Archiver::PAGE_URLS_RECORD_NAME); + } finally { + $this->restoreGeneralConfig($configBackup); + } + + $flatExport = $this->exportFlatTableValues($flatUrls); + $hierarchicalExport = $this->exportHierarchyTableValues($hierarchicalUrls); + + $this->assertCount(1, $flatExport['rows']); + $this->assertArrayHasKey('/shared/path', $flatExport['rows']); + $flatUrlRow = $flatUrls->getRowFromLabel('/shared/path'); + $this->assertNotFalse($flatUrlRow); + $this->assertNotFalse($flatUrlRow->getMetadata('url')); + $this->assertCount(1, $hierarchicalExport['rows']); + $hierarchicalRows = array_values($hierarchicalExport['rows']); + $this->assertCount(1, $hierarchicalRows); + $this->assertSame(2, $hierarchicalRows[0][Metrics::INDEX_PAGE_NB_HITS]); + } + + protected static function configureFixture($fixture) + { + parent::configureFixture($fixture); + $fixture->createSuperUser = true; + } + + private function trackUrlHits(int $idSite, string $day, array $urlHits): void + { + $tracker = Fixture::getTracker($idSite, $day . ' 00:00:01'); + $second = 1; + + foreach ($urlHits as $path => $hits) { + for ($i = 0; $i < $hits; ++$i) { + $tracker->setForceVisitDateTime($day . sprintf(' 00:00:%02d', $second)); + $tracker->setUrl('http://example.org' . $path); + Fixture::checkResponse($tracker->doTrackPageView('title ' . $path)); + ++$second; + } + } + } + + private function backupGeneralConfig(array $configKeys): array + { + $config = Config::getInstance(); + $backup = []; + foreach ($configKeys as $key) { + $exists = array_key_exists($key, $config->General); + $backup[$key] = [ + 'exists' => $exists, + 'value' => $exists ? $config->General[$key] : null, + ]; + } + + return $backup; + } + + private function restoreGeneralConfig(array $configBackup): void + { + $config = Config::getInstance(); + foreach ($configBackup as $key => $state) { + if (!empty($state['exists'])) { + $config->General[$key] = $state['value']; + } else { + unset($config->General[$key]); + } + } + } + + private function assertDayArchiveRecordNames(int $idSite, string $day, array $expectedPresent, array $expectedAbsent): void + { + $table = Common::prefixTable('archive_blob_2026_01'); + $names = Db::fetchAll( + "SELECT DISTINCT name + FROM $table + WHERE idsite = ? + AND period = 1 + AND date1 = ? + AND date2 = ? + AND name IN (?, ?)", + [$idSite, $day, $day, Archiver::PAGE_URLS_RECORD_NAME, Archiver::PAGE_URLS_FLAT_RECORD_NAME] + ); + $names = array_column($names, 'name'); + + foreach ($expectedPresent as $name) { + $this->assertContains($name, $names); + } + foreach ($expectedAbsent as $name) { + $this->assertNotContains($name, $names); + } + } + + private function getSummaryHits(DataTable $table): int + { + $summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW); + if ($summaryRow === false) { + return 0; + } + + return (int) $summaryRow->getColumn(Metrics::INDEX_PAGE_NB_HITS); + } + + private function sumSummaryHitsRecursively(DataTable $table): int + { + $sum = $this->getSummaryHits($table); + foreach ($table->getRowsWithoutSummaryRow() as $row) { + /** @var Row $row */ + $subtable = $row->getSubtable(); + if ($subtable instanceof DataTable) { + $sum += $this->sumSummaryHitsRecursively($subtable); + } + } + + return $sum; + } + + private function sumHitsWithoutSummary(DataTable $table): int + { + $sum = 0; + foreach ($table->getRowsWithoutSummaryRow() as $row) { + /** @var Row $row */ + $sum += (int) $row->getColumn(Metrics::INDEX_PAGE_NB_HITS); + } + + return $sum; + } + + private function exportHierarchyTableValues(DataTable $hierarchicalTable): array + { + $flattened = new DataTable(); + ArchivingHelper::mergeHierarchicalActionsTableIntoFlatTable($hierarchicalTable, $flattened); + + return $this->exportFlatTableValues($flattened); + } + + private function exportFlatTableValues(DataTable $flatTable): array + { + $rows = []; + foreach ($flatTable->getRowsWithoutSummaryRow() as $row) { + $label = $row->getColumn('label'); + if (!is_string($label)) { + continue; + } + + $columns = $row->getColumns(); + unset($columns['label']); + ksort($columns); + $rows[$label] = $columns; + } + ksort($rows); + + $summaryColumns = []; + $summaryRow = $flatTable->getRowFromId(DataTable::ID_SUMMARY_ROW); + if ($summaryRow !== false) { + $summaryColumns = $summaryRow->getColumns(); + unset($summaryColumns['label']); + ksort($summaryColumns); + } + + return [ + 'rows' => $rows, + 'summary' => $summaryColumns, + ]; + } +} diff --git a/plugins/Actions/tests/UI/ActionsDataTable_spec.js b/plugins/Actions/tests/UI/ActionsDataTable_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..d3b6111bebc9c1b55259b77793b39bff11e03741 --- /dev/null +++ b/plugins/Actions/tests/UI/ActionsDataTable_spec.js @@ -0,0 +1,145 @@ +/*! + * Matomo - free/libre analytics platform + * + * ActionsDataTable screenshot tests. + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe("ActionsDataTable", function () { + const url = "?module=Widgetize&action=iframe&idSite=1&period=year&date=2012-08-09&moduleToWidgetize=Actions&actionToWidgetize=getPageUrls&isFooterExpandedInDashboard=1"; + + it("should load correctly", async function() { + await page.goto(url); + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('initial'); + }); + + it("should sort column correctly when column header clicked", async function() { + await page.click('th#avg_time_on_page'); + await page.mouse.move(-10, -10); + await page.waitForNetworkIdle(); + expect(await page.screenshot({ fullPage: true })).to.matchImage('column_sorted'); + }); + + it("should load subtables correctly when row clicked", async function() { + secondRow = await page.jQuery('tr.subDataTable:eq(2)'); + await secondRow.click(); + firstRow = await page.jQuery('tr.subDataTable:first'); + await firstRow.click(); + await page.mouse.move(-10, -10); + + await page.waitForNetworkIdle(); + await page.waitForTimeout(250); // rendering + + expect(await page.screenshot({ fullPage: true })).to.matchImage('subtables_loaded'); + }); + + it("should show configuration options", async function() { + await page.click('.dropdownConfigureIcon'); + await page.mouse.move(-10, -10); + const element = await page.$('.tableConfiguration'); + await page.waitForTimeout(250); // rendering + expect(await element.screenshot()).to.matchImage('configuration_options'); + }); + + it("should flatten table when flatten link clicked", async function() { + await page.click('.dataTableFlatten'); + await page.mouse.move(-10, -10); + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('flattened'); + }); + + it("should exclude low population rows when exclude low population link clicked", async function() { + await page.click('.dropdownConfigureIcon'); + await page.click('.dataTableExcludeLowPopulation'); + await page.mouse.move(-10, -10); + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('exclude_low_population'); + }); + + it("should load normal view when switch to view hierarchical view link is clicked", async function() { + await page.click('.dropdownConfigureIcon'); + await page.click('.dataTableFlatten'); + await page.waitForNetworkIdle(); + await page.mouse.move(-10, -10); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('unflattened'); + }); + + it("should display pageview percentages when hovering over pageviews column", async function() { + const elem = await page.jQuery('tr:contains("index.htm") td.column:eq(1)'); + await elem.hover(); + expect(await page.screenshot({ fullPage: true })).to.matchImage('pageview_percentages'); + }); + + it("should generate a proper title for the visitor log segmented by the current row", async function() { + await page.mouse.move(-10, -10); + const row = 'tr:contains("index.htm") '; + const first = await page.jQuery(row + 'td.column:first'); + await first.hover(); + const second = await page.jQuery(row + 'td.label .actionSegmentVisitorLog'); + await second.hover(); + expect(await page.screenshot({ fullPage: true })).to.matchImage('segmented_visitor_log_hover'); + }); + + it("should open the visitor log segmented by the current row", async function() { + await page.evaluate(function(){ + $('tr:contains("index.htm") td.label .actionSegmentVisitorLog').click(); + }); + await page.mouse.move(-10, -10); + await page.waitForSelector('.ui-dialog'); + await page.waitForNetworkIdle(); + const element = await page.$('.ui-dialog'); + expect(await element.screenshot()).to.matchImage('segmented_visitor_log'); + }); + + it("should display unique pageview percentages when hovering over unique pageviews column", async function() { + await page.click('.ui-widget .ui-dialog-titlebar-close'); + const elem = await page.jQuery('tr:contains("index.htm") td.column:eq(2)'); + await elem.hover(); + await page.waitForTimeout(100); + expect(await page.screenshot({ fullPage: true })).to.matchImage('unique_pageview_percentages'); + }); + + it("should show the search when clicking on the search icon", async function() { + await page.click('.dataTableAction.searchAction'); + await page.mouse.move(-10, -10); + await page.waitForTimeout(500); + expect(await page.screenshot({ fullPage: true })).to.matchImage('search_visible'); + }); + + it("should search through table when search input entered and search button clicked and input should be visible", async function() { + await page.type('.searchAction .dataTableSearchInput', 'i'); + await page.click('.searchAction .icon-search'); + await page.waitForNetworkIdle(); + await page.mouse.move(-10, -10); + expect(await page.screenshot({ fullPage: true })).to.matchImage('search'); + }); + + it("should close search when clicking on the x icon", async function() { + await page.click('.searchAction .icon-close'); + await page.waitForNetworkIdle(); + await page.mouse.move(-10, -10); + expect(await page.screenshot({ fullPage: true })).to.matchImage('search_closed'); + }); + + it("should automatically expand subtables if it contains only one folder", async function() { + await page.goto(url + '&viewDataTable=table'); + + await page.waitForFunction("$('tr .value:contains(\"blog\")').length > 0"); + const first = await page.jQuery('tr .value:contains("blog")'); + await first.click(); + await page.waitForFunction("$('tr .value:contains(\"2012\")').length > 0"); + const second = await page.jQuery('tr .value:contains("2012")'); + await second.click(); + + await page.waitForNetworkIdle(); + + expect(await page.screenshot({ fullPage: true })).to.matchImage('auto_expand'); + }); +}); diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png new file mode 100644 index 0000000000000000000000000000000000000000..2ae686af08e6e0be48054164bc2674cb8876c8fb Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png new file mode 100644 index 0000000000000000000000000000000000000000..e47dcc32e63eba25cf3e7464cc0433ab95ae32a3 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_configuration_options.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_configuration_options.png new file mode 100644 index 0000000000000000000000000000000000000000..43e0c4937716a1b09a3bf270129da8780d325fb0 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_configuration_options.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png new file mode 100644 index 0000000000000000000000000000000000000000..bedeb615ddcb9c279329feef5dacc65702536820 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png new file mode 100644 index 0000000000000000000000000000000000000000..c0b03ef11368f3cd806f8b6e99e079f6ec05d414 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png new file mode 100644 index 0000000000000000000000000000000000000000..2c194fea512e8a3278fd9eb1eaaeecd6f2283e46 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png new file mode 100644 index 0000000000000000000000000000000000000000..3c9e71aebe3e32d7ecc37745f598c507e43397a1 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png new file mode 100644 index 0000000000000000000000000000000000000000..0a49b95c745fa1dc04e7a367df1cf3ad58b75707 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..2b74178518264496ba34339970de2d542809d249 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png new file mode 100644 index 0000000000000000000000000000000000000000..89b8d14edf214a87ea9f90087a15ab5a39902090 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log.png new file mode 100644 index 0000000000000000000000000000000000000000..3b49ddfe3cd80506f6190be4fed24cffd4797bf8 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..b009a727a08275108c4f74634b034796674f8359 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png new file mode 100644 index 0000000000000000000000000000000000000000..b11a607002a04261384b592c29bb5a28c395d39f Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png new file mode 100644 index 0000000000000000000000000000000000000000..2b74178518264496ba34339970de2d542809d249 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png differ diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png new file mode 100644 index 0000000000000000000000000000000000000000..4165ce890997f2fb49a17f65bf7393634b3d4573 Binary files /dev/null and b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png differ diff --git a/plugins/Actions/tests/Unit/ActionReportsTest.php b/plugins/Actions/tests/Unit/ActionReportsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d87575bb1bbc754a18610fd7351234a7cee94c11 --- /dev/null +++ b/plugins/Actions/tests/Unit/ActionReportsTest.php @@ -0,0 +1,678 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\tests\Unit; + +use Piwik\ArchiveProcessor; +use Piwik\Config; +use Piwik\DataTable; +use Piwik\DataTable\Row; +use Piwik\Date; +use Piwik\Plugins\Actions\Archiver; +use Piwik\Plugins\Actions\ArchivingHelper; +use Piwik\Plugins\Actions\RecordBuilders\ActionReports; +use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; + +/** + * @group Actions + * @group Plugins + */ +class ActionReportsTest extends \PHPUnit\Framework\TestCase +{ + public function testGetRecordMetadataDoesNotIncludeFlatRecordsWhenFlatLimitIsZero() + { + $this->withFlatLimit(0, function () { + $recordBuilder = new ActionReports(); + $records = $recordBuilder->getRecordMetadata($this->createMock(ArchiveProcessor::class)); + $recordNames = array_map(function ($record) { + return $record->getName(); + }, $records); + + $this->assertNotContains(Archiver::PAGE_URLS_FLAT_RECORD_NAME, $recordNames); + $this->assertNotContains(Archiver::PAGE_TITLES_FLAT_RECORD_NAME, $recordNames); + }); + } + + public function testBuildForNonDayPeriodUsesLegacyPathWhenFlatArchivingDisabled() + { + $this->withFlatLimit(0, function () { + $params = $this->createParams( + [Archiver::PAGE_URLS_RECORD_NAME], + [], + ['2026-01-01'] + ); + $archiveProcessor = $this->getMockBuilder(ArchiveProcessor::class) + ->disableOriginalConstructor() + ->onlyMethods(['getParams', 'aggregateDataTableRecords', 'aggregateNumericMetrics', 'insertNumericRecords']) + ->getMock(); + $archiveProcessor->method('getParams')->willReturn($params); + $archiveProcessor->expects($this->never())->method('aggregateNumericMetrics'); + $archiveProcessor->expects($this->never())->method('insertNumericRecords'); + + $aggregatedRecordNames = []; + $archiveProcessor->expects($this->once())->method('aggregateDataTableRecords') + ->willReturnCallback(function (...$args) use (&$aggregatedRecordNames) { + $aggregatedRecordNames[] = $args[0]; + return []; + }); + + $recordBuilder = new ActionReports(); + $recordBuilder->buildForNonDayPeriod($archiveProcessor); + + $this->assertSame([Archiver::PAGE_URLS_RECORD_NAME], $aggregatedRecordNames); + }); + } + + public function testBuildForNonDayPeriodFlatFirstAggregatesMixedFlatAndHierarchicalSources() + { + $this->withFlatLimit(50000, function () { + $flatRecordName = Archiver::PAGE_URLS_FLAT_RECORD_NAME; + $hierarchicalRecordName = Archiver::PAGE_URLS_RECORD_NAME; + + $flatPeriodTable = $this->createFlatSerializedTable('/flat-a', ['/flat-a'], 4); + $hierarchicalPeriodTable = $this->createHierarchicalSerializedTable('/legacy-b', 6, 2); + + $rowsByRecordName = [ + $flatRecordName => [[ + 'date1' => '2026-01-01', + 'date2' => '2026-01-01', + 'name' => $flatRecordName, + 'value' => $flatPeriodTable, + ]], + $hierarchicalRecordName => [[ + 'date1' => '2026-01-02', + 'date2' => '2026-01-02', + 'name' => $hierarchicalRecordName, + 'value' => $hierarchicalPeriodTable, + ]], + ]; + + $recordBuilder = new class ($rowsByRecordName) extends ActionReports { + private $rowsByRecordName; + + public function __construct(array $rowsByRecordName) + { + $this->rowsByRecordName = $rowsByRecordName; + parent::__construct(); + } + + protected function querySingleBlobRows(ArchiveProcessor $archiveProcessor, string $recordName): iterable + { + return $this->rowsByRecordName[$recordName] ?? []; + } + }; + + $params = $this->createParams( + [Archiver::PAGE_URLS_RECORD_NAME], + [], + ['2026-01-01', '2026-01-02'] + ); + $archiveProcessor = $this->getMockBuilder(ArchiveProcessor::class) + ->disableOriginalConstructor() + ->onlyMethods(['getParams', 'insertBlobRecord', 'aggregateDataTableRecords', 'aggregateNumericMetrics', 'insertNumericRecords']) + ->getMock(); + $archiveProcessor->method('getParams')->willReturn($params); + $archiveProcessor->expects($this->never())->method('aggregateDataTableRecords'); + $archiveProcessor->expects($this->never())->method('aggregateNumericMetrics'); + $archiveProcessor->expects($this->never())->method('insertNumericRecords'); + + $insertedBlobs = []; + $archiveProcessor->method('insertBlobRecord')->willReturnCallback(function ($recordName, $blobValue) use (&$insertedBlobs) { + $insertedBlobs[$recordName] = $blobValue; + }); + + $recordBuilder->buildForNonDayPeriod($archiveProcessor); + + $this->assertArrayHasKey($flatRecordName, $insertedBlobs); + $this->assertArrayHasKey($hierarchicalRecordName, $insertedBlobs); + $this->assertArrayNotHasKey(Archiver::PAGE_TITLES_RECORD_NAME, $insertedBlobs); + + $flatResult = DataTable::fromSerializedArray( + $this->getRootBlobFromInsertedRecord($insertedBlobs[$flatRecordName], $flatRecordName) + ); + $flatRowA = $flatResult->getRowFromLabel('/flat-a'); + $this->assertNotFalse($flatRowA); + $this->assertSame(4, $flatRowA->getColumn('nb_hits')); + + $flatRowB = $flatResult->getRowFromLabel('/legacy-b'); + $this->assertNotFalse($flatRowB); + $this->assertSame(6, $flatRowB->getColumn('nb_hits')); + + $flatSummary = $flatResult->getRowFromId(DataTable::ID_SUMMARY_ROW); + $this->assertNotFalse($flatSummary); + $this->assertSame(2, $flatSummary->getColumn('nb_hits')); + + $hierarchicalResult = DataTable::fromSerializedArray( + $this->getRootBlobFromInsertedRecord($insertedBlobs[$hierarchicalRecordName], $hierarchicalRecordName) + ); + $hierarchicalRowA = $hierarchicalResult->getRowFromLabel('/flat-a'); + $this->assertNotFalse($hierarchicalRowA); + $this->assertSame(4, $hierarchicalRowA->getColumn('nb_hits')); + + $hierarchicalRowB = $hierarchicalResult->getRowFromLabel('/legacy-b'); + $this->assertNotFalse($hierarchicalRowB); + $this->assertSame(6, $hierarchicalRowB->getColumn('nb_hits')); + + $hierarchicalSummary = $hierarchicalResult->getRowFromId(DataTable::ID_SUMMARY_ROW); + $this->assertNotFalse($hierarchicalSummary); + $this->assertSame(2, $hierarchicalSummary->getColumn('nb_hits')); + }); + } + + public function testBuildForNonDayPeriodFlatFirstAggregatesMixedSourcesWhenOnlyFlatRecordIsRequested() + { + $this->withFlatLimit(50000, function () { + $flatRecordName = Archiver::PAGE_URLS_FLAT_RECORD_NAME; + $hierarchicalRecordName = Archiver::PAGE_URLS_RECORD_NAME; + + $flatPeriodTable = $this->createFlatSerializedTable('/flat-a', ['/flat-a'], 4); + $hierarchicalPeriodTable = $this->createHierarchicalSerializedTable('/legacy-b', 6, 2); + + $rowsByRecordName = [ + $flatRecordName => [[ + 'date1' => '2026-01-01', + 'date2' => '2026-01-01', + 'name' => $flatRecordName, + 'value' => $flatPeriodTable, + ]], + $hierarchicalRecordName => [[ + 'date1' => '2026-01-02', + 'date2' => '2026-01-02', + 'name' => $hierarchicalRecordName, + 'value' => $hierarchicalPeriodTable, + ]], + ]; + + $recordBuilder = new class ($rowsByRecordName) extends ActionReports { + private $rowsByRecordName; + + public function __construct(array $rowsByRecordName) + { + $this->rowsByRecordName = $rowsByRecordName; + parent::__construct(); + } + + protected function querySingleBlobRows(ArchiveProcessor $archiveProcessor, string $recordName): iterable + { + return $this->rowsByRecordName[$recordName] ?? []; + } + }; + + $params = $this->createParams( + [Archiver::PAGE_URLS_FLAT_RECORD_NAME], + [Archiver::PAGE_URLS_RECORD_NAME], + ['2026-01-01', '2026-01-02'] + ); + $archiveProcessor = $this->getMockBuilder(ArchiveProcessor::class) + ->disableOriginalConstructor() + ->onlyMethods(['getParams', 'insertBlobRecord', 'aggregateDataTableRecords', 'aggregateNumericMetrics', 'insertNumericRecords']) + ->getMock(); + $archiveProcessor->method('getParams')->willReturn($params); + $archiveProcessor->expects($this->never())->method('aggregateDataTableRecords'); + $archiveProcessor->expects($this->never())->method('aggregateNumericMetrics'); + $archiveProcessor->expects($this->never())->method('insertNumericRecords'); + + $insertedBlobs = []; + $archiveProcessor->method('insertBlobRecord')->willReturnCallback(function ($recordName, $blobValue) use (&$insertedBlobs) { + $insertedBlobs[$recordName] = $blobValue; + }); + + $recordBuilder->buildForNonDayPeriod($archiveProcessor); + + $this->assertArrayHasKey($flatRecordName, $insertedBlobs); + $this->assertArrayHasKey($hierarchicalRecordName, $insertedBlobs); + + $flatResult = DataTable::fromSerializedArray( + $this->getRootBlobFromInsertedRecord($insertedBlobs[$flatRecordName], $flatRecordName) + ); + $flatRowA = $flatResult->getRowFromLabel('/flat-a'); + $this->assertNotFalse($flatRowA); + $this->assertSame(4, $flatRowA->getColumn('nb_hits')); + + $flatRowB = $flatResult->getRowFromLabel('/legacy-b'); + $this->assertNotFalse($flatRowB); + $this->assertSame(6, $flatRowB->getColumn('nb_hits')); + + $flatSummary = $flatResult->getRowFromId(DataTable::ID_SUMMARY_ROW); + $this->assertNotFalse($flatSummary); + $this->assertSame(2, $flatSummary->getColumn('nb_hits')); + }); + } + + public function testMergeHierarchicalActionsTableIntoFlatTableMovesNestedOthersToGlobalOthers() + { + $hierarchical = new DataTable(); + $rootA = $hierarchical->addRow(new Row([Row::COLUMNS => ['label' => 'a', 'nb_hits' => 8]])); + $rootB = $hierarchical->addRow(new Row([Row::COLUMNS => ['label' => 'b', 'nb_hits' => 2]])); + $hierarchical->addSummaryRow(new Row([Row::COLUMNS => ['label' => DataTable::LABEL_SUMMARY_ROW, 'nb_hits' => 7]])); + + $subtableA = new DataTable(); + $subtableA->addRow(new Row([Row::COLUMNS => ['label' => '/x', 'nb_hits' => 3]])); + $subtableA->addSummaryRow(new Row([Row::COLUMNS => ['label' => DataTable::LABEL_SUMMARY_ROW, 'nb_hits' => 5]])); + $rootA->setSubtable($subtableA); + + $flat = new DataTable(); + ArchivingHelper::mergeHierarchicalActionsTableIntoFlatTable($hierarchical, $flat); + + $flatSummary = $flat->getRowFromId(DataTable::ID_SUMMARY_ROW); + $this->assertNotFalse($flatSummary); + $this->assertSame(12, $flatSummary->getColumn('nb_hits')); + + $flatRowA = $flat->getRowFromLabel(json_encode(['a', '/x'])); + $this->assertNotFalse($flatRowA); + $this->assertSame(3, $flatRowA->getColumn('nb_hits')); + + $flatRowB = $flat->getRowFromLabel(json_encode(['b'])); + $this->assertNotFalse($flatRowB); + $this->assertSame(2, $flatRowB->getColumn('nb_hits')); + + $recordBuilder = new class extends ActionReports { + public function buildHierarchyFromFlatForTest(DataTable $flat): DataTable + { + return $this->buildHierarchicalTableFromFlatTable( + $flat, + null, + function (Row $flatRow) { + $path = $flatRow->getMetadata(ArchivingHelper::ACTION_FLAT_PATH_METADATA_NAME); + return is_array($path) && !empty($path) ? $path : null; + } + ); + } + }; + + $rebuilt = $recordBuilder->buildHierarchyFromFlatForTest($flat); + $rebuiltSummary = $rebuilt->getRowFromId(DataTable::ID_SUMMARY_ROW); + $this->assertNotFalse($rebuiltSummary); + $this->assertSame(12, $rebuiltSummary->getColumn('nb_hits')); + } + + public function testPageUrlFlatRowsDeduplicateUsingNormalizedUrl() + { + $table = new DataTable(); + + $firstRow = $this->invokeGetFlatActionRow( + 'EXAMPLE.org/shared/path#', + Action::TYPE_PAGE_URL, + PageUrl::$urlPrefixMap['http://'], + $table + ); + $secondRow = $this->invokeGetFlatActionRow( + 'example.org/shared/path', + Action::TYPE_PAGE_URL, + PageUrl::$urlPrefixMap['http://'], + $table + ); + + $this->assertSame($firstRow, $secondRow); + $this->assertSame('/shared/path', $firstRow->getColumn('label')); + $this->assertSame(1, $table->getRowsCount()); + $this->assertFalse($firstRow->getMetadata(ArchivingHelper::ACTION_FLAT_PATH_METADATA_NAME)); + } + + public function testPageUrlFlatRowsUseUrlPrefixToBuildCanonicalPathLabel() + { + $table = new DataTable(); + + $row = $this->invokeGetFlatActionRow( + 'example.org/page', + Action::TYPE_PAGE_URL, + PageUrl::$urlPrefixMap['http://www.'], + $table + ); + + $this->assertSame('/page', $row->getColumn('label')); + $this->assertSame(1, $table->getRowsCount()); + } + + public function testPageUrlFlatRowsTreatStringActionTypeAsPageUrl() + { + $table = new DataTable(); + + $row = $this->invokeGetFlatActionRow( + 'example.org/page', + '1', + PageUrl::$urlPrefixMap['http://www.'], + $table + ); + + $this->assertSame('/page', $row->getColumn('label')); + $this->assertSame(1, $table->getRowsCount()); + } + + public function testMergingFlatRowsUsesLastUrlMetadata() + { + $destination = new Row([ + Row::COLUMNS => ['label' => '/page', 'nb_hits' => 1], + ]); + $destination->setMetadata('url', 'http://example.org/first'); + + $source = new Row([ + Row::COLUMNS => ['label' => '/page', 'nb_hits' => 1], + ]); + $source->setMetadata('url', 'http://example.org/last'); + + $method = new \ReflectionMethod(ArchivingHelper::class, 'mergeRowIntoDestination'); + $method->setAccessible(true); + $method->invoke(null, $source, $destination); + + $this->assertSame('http://example.org/last', $destination->getMetadata('url')); + } + + public function testUnknownPageUrlFlatLabelRebuildsWithoutLeadingSlash() + { + $flatRow = new Row([ + Row::COLUMNS => [ + 'label' => ArchivingHelper::getUnknownActionName(Action::TYPE_PAGE_URL), + 'nb_hits' => 1, + ], + ]); + + $recordBuilder = new ActionReports(); + $method = new \ReflectionMethod(ActionReports::class, 'flatRowToHierarchyPath'); + $method->setAccessible(true); + $path = $method->invoke($recordBuilder, $flatRow, Action::TYPE_PAGE_URL); + + $this->assertSame([ArchivingHelper::getUnknownActionName(Action::TYPE_PAGE_URL)], $path); + } + + public function testPageUrlFlatLabelRebuildPreservesTrailingIndexAtLevelLimit() + { + $this->withActionCategoryLevelLimit(10, function () { + $flatRow = new Row([ + Row::COLUMNS => [ + 'label' => '/this/is/not/the/page/i/am/looking/for/index', + 'nb_hits' => 1, + ], + ]); + + $recordBuilder = new ActionReports(); + $method = new \ReflectionMethod(ActionReports::class, 'flatRowToHierarchyPath'); + $method->setAccessible(true); + $path = $method->invoke($recordBuilder, $flatRow, Action::TYPE_PAGE_URL); + + $this->assertSame( + ['this', 'is', 'not', 'the', 'page', 'i', 'am', 'looking', 'for', '/index'], + $path + ); + }); + } + + public function testHierarchyBuiltFromCanonicalFlatUrlLabelMatchesFlatRowCount() + { + $flat = new DataTable(); + + $firstRow = new Row([Row::COLUMNS => ['label' => '/shared/path', 'nb_hits' => 4]]); + $flat->addRow($firstRow); + + $recordBuilder = new class extends ActionReports { + public function buildUrlHierarchyFromFlatForTest(DataTable $flat): DataTable + { + return $this->buildHierarchicalTableFromFlatTable( + $flat, + null, + [$this, 'flatRowToUrlHierarchyPath'] + ); + } + }; + + $rebuilt = $recordBuilder->buildUrlHierarchyFromFlatForTest($flat); + + $this->assertSame(1, $flat->getRowsCount()); + $this->assertSame(1, $rebuilt->getRowsCount()); + + $parentRow = $rebuilt->getRows()[0] ?? null; + $this->assertInstanceOf(Row::class, $parentRow); + $this->assertSame('shared', $parentRow->getColumn('label')); + + $leafTable = $parentRow->getSubtable(); + $this->assertInstanceOf(DataTable::class, $leafTable); + + $leafRow = $leafTable->getRows()[0] ?? null; + $this->assertInstanceOf(Row::class, $leafRow); + $this->assertSame('/path', $leafRow->getColumn('label')); + $this->assertSame(4, $leafRow->getColumn('nb_hits')); + } + + public function testPageTitleFlatLabelCanBeRebuiltIntoTheSameHierarchy() + { + $this->withTitleDelimiter(' / ', function () { + $flat = new DataTable(); + $flat->addRow(new Row([Row::COLUMNS => ['label' => 'Parent / Child', 'nb_hits' => 3]])); + + $recordBuilder = new class extends ActionReports { + public function buildTitleHierarchyFromFlatForTest(DataTable $flat): DataTable + { + return $this->buildHierarchicalTableFromFlatTable( + $flat, + null, + [$this, 'flatRowToTitleHierarchyPath'] + ); + } + }; + + $rebuilt = $recordBuilder->buildTitleHierarchyFromFlatForTest($flat); + + $parentRow = $rebuilt->getRows()[0] ?? null; + $this->assertInstanceOf(Row::class, $parentRow); + $this->assertSame('Parent', $parentRow->getColumn('label')); + + $leafTable = $parentRow->getSubtable(); + $this->assertInstanceOf(DataTable::class, $leafTable); + + $leafRow = $leafTable->getRows()[0] ?? null; + $this->assertInstanceOf(Row::class, $leafRow); + $this->assertSame(' Child', $leafRow->getColumn('label')); + $this->assertSame(3, $leafRow->getColumn('nb_hits')); + }); + } + + private function withFlatLimit(int $flatLimit, callable $callback): void + { + $config = Config::getInstance(); + $hadPreviousFlatLimit = array_key_exists('datatable_archiving_maximum_rows_actions_flat', $config->General); + $previousFlatLimit = $hadPreviousFlatLimit ? $config->General['datatable_archiving_maximum_rows_actions_flat'] : null; + + $config->General['datatable_archiving_maximum_rows_actions_flat'] = $flatLimit; + ArchivingHelper::reloadConfig(); + + try { + $callback(); + } finally { + if ($hadPreviousFlatLimit) { + $config->General['datatable_archiving_maximum_rows_actions_flat'] = $previousFlatLimit; + } else { + unset($config->General['datatable_archiving_maximum_rows_actions_flat']); + } + ArchivingHelper::reloadConfig(); + } + } + + private function withTitleDelimiter(string $titleDelimiter, callable $callback): void + { + $config = Config::getInstance(); + $hadPreviousDelimiter = array_key_exists('action_title_category_delimiter', $config->General); + $previousDelimiter = $hadPreviousDelimiter ? $config->General['action_title_category_delimiter'] : null; + + $config->General['action_title_category_delimiter'] = $titleDelimiter; + ArchivingHelper::reloadConfig(); + + try { + $callback(); + } finally { + if ($hadPreviousDelimiter) { + $config->General['action_title_category_delimiter'] = $previousDelimiter; + } else { + unset($config->General['action_title_category_delimiter']); + } + ArchivingHelper::reloadConfig(); + } + } + + private function withActionCategoryLevelLimit(int $levelLimit, callable $callback): void + { + $config = Config::getInstance(); + $hadPreviousLimit = array_key_exists('action_category_level_limit', $config->General); + $previousLimit = $hadPreviousLimit ? $config->General['action_category_level_limit'] : null; + + $config->General['action_category_level_limit'] = $levelLimit; + ArchivingHelper::reloadConfig(); + + try { + $callback(); + } finally { + if ($hadPreviousLimit) { + $config->General['action_category_level_limit'] = $previousLimit; + } else { + unset($config->General['action_category_level_limit']); + } + ArchivingHelper::reloadConfig(); + } + } + + private function createParams(array $requestedReports, array $foundRequestedReports, array $dates): object + { + $subperiods = array_map(function ($date) { + return new class ($date) { + private $date; + + public function __construct(string $date) + { + $this->date = $date; + } + + public function getDateStart(): Date + { + return Date::factory($this->date); + } + + public function getDateEnd(): Date + { + return Date::factory($this->date); + } + }; + }, $dates); + + $period = new class ($subperiods) { + private $subperiods; + + public function __construct(array $subperiods) + { + $this->subperiods = $subperiods; + } + + public function getSubperiods(): array + { + return $this->subperiods; + } + }; + + $site = new class { + public function getId(): int + { + return 1; + } + + public function getMainUrl(): string + { + return 'https://example.test/'; + } + }; + + return new class ($requestedReports, $foundRequestedReports, $period, $site) { + private $requestedReports; + private $foundRequestedReports; + private $period; + private $site; + + public function __construct(array $requestedReports, array $foundRequestedReports, object $period, object $site) + { + $this->requestedReports = $requestedReports; + $this->foundRequestedReports = $foundRequestedReports; + $this->period = $period; + $this->site = $site; + } + + public function getArchiveOnlyReportAsArray(): array + { + return $this->requestedReports; + } + + public function getFoundRequestedReports(): array + { + return $this->foundRequestedReports; + } + + public function getPeriod(): object + { + return $this->period; + } + + public function getSite(): object + { + return $this->site; + } + + public function getSegment() + { + return null; + } + }; + } + + private function createFlatSerializedTable(string $label, array $actionPath, int $nbHits): string + { + $flat = new DataTable(); + $flatRow = new Row([Row::COLUMNS => ['label' => $label, 'nb_hits' => $nbHits]]); + $flatRow->setMetadata(ArchivingHelper::ACTION_FLAT_PATH_METADATA_NAME, $actionPath); + $flat->addRow($flatRow); + + return $this->getRootSerializedBlob($flat); + } + + private function createHierarchicalSerializedTable(string $label, int $nbHits, int $summaryHits): string + { + $table = new DataTable(); + $table->addRow(new Row([Row::COLUMNS => ['label' => $label, 'nb_hits' => $nbHits]])); + $table->addSummaryRow(new Row([Row::COLUMNS => ['label' => DataTable::LABEL_SUMMARY_ROW, 'nb_hits' => $summaryHits]])); + + return $this->getRootSerializedBlob($table); + } + + private function getRootSerializedBlob(DataTable $table): string + { + $serialized = $table->getSerialized(null, null, null); + if (!is_array($serialized)) { + return $serialized; + } + + return (string) reset($serialized); + } + + private function getRootBlobFromInsertedRecord($blobValue, string $recordName): string + { + if (!is_array($blobValue)) { + return $blobValue; + } + + if (!empty($blobValue[$recordName])) { + return $blobValue[$recordName]; + } + + return (string) reset($blobValue); + } + + private function invokeGetFlatActionRow(string $actionName, int $actionType, int $urlPrefix, DataTable $table): Row + { + $reflection = new \ReflectionMethod(ArchivingHelper::class, 'getFlatActionRow'); + $reflection->setAccessible(true); + + return $reflection->invoke(null, $actionName, $actionType, $urlPrefix, $table); + } +} diff --git a/plugins/Actions/tests/Unit/ArchiverTest.php b/plugins/Actions/tests/Unit/ArchiverTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a768348579672df9a0567e89190a84b02f674dee --- /dev/null +++ b/plugins/Actions/tests/Unit/ArchiverTest.php @@ -0,0 +1,200 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\tests\Unit; + +use Piwik\DataTable; +use Piwik\DataTable\Row; +use Piwik\Metrics as PiwikMetrics; +use Piwik\Plugins\Actions\ArchivingHelper; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tracker\Action; + +require_once PIWIK_INCLUDE_PATH . '/plugins/Actions/Actions.php'; + +/** + * @group Actions + * @group ArchiverTest + * @group Plugins + */ +class ArchiverTests extends \PHPUnit\Framework\TestCase +{ + public function setUp(): void + { + Fixture::loadAllTranslations(); + } + + public function tearDown(): void + { + Fixture::resetTranslations(); + } + + public function getActionNameTestData() + { + return array( + array( + 'params' => array('name' => 'http://example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => null), + 'expected' => array('/index'), + ), + array( + 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 1), + 'expected' => array('/index'), + ), + array( + 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 2), + 'expected' => array('/index'), + ), + array( + 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 3), + 'expected' => array('/index'), + ), + array( + 'params' => array('name' => 'example.org/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 4), + 'expected' => array('/index'), + ), + array( + 'params' => array('name' => 'example.org/path/', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 4), + 'expected' => array('path', '/index'), + ), + array( + 'params' => array('name' => 'example.org/test/path', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 1), + 'expected' => array('test', '/path'), + ), + array( + 'params' => array('name' => 'http://example.org/path/', 'type' => Action::TYPE_PAGE_URL), + 'expected' => array('path', '/index'), + ), + array( + 'params' => array('name' => 'example.org/test/path', 'type' => Action::TYPE_PAGE_URL, 'urlPrefix' => 1), + 'expected' => array('test', '/path'), + ), + array( + 'params' => array('name' => 'Test / Path', 'type' => Action::TYPE_PAGE_URL), + 'expected' => array('Test', '/Path'), + ), + array( + 'params' => array('name' => ' Test trim ', 'type' => Action::TYPE_PAGE_URL), + 'expected' => array('/Test trim'), + ), + array( + 'params' => array('name' => 'Category / Subcategory', 'type' => Action::TYPE_PAGE_TITLE), + 'expected' => array(' Category / Subcategory'), + ), + array( + 'params' => array('name' => '/path/index.php?var=test', 'type' => Action::TYPE_PAGE_TITLE), + 'expected' => array(' /path/index.php?var=test'), + ), + array( + 'params' => array('name' => 'http://example.org/path/Default.aspx#anchor', 'type' => Action::TYPE_PAGE_TITLE), + 'expected' => array(' http://example.org/path/Default.aspx#anchor'), + ), + array( + 'params' => array('name' => '', 'type' => Action::TYPE_PAGE_TITLE), + 'expected' => array(' Page Name not defined'), + ), + array( + 'params' => array('name' => '', 'type' => Action::TYPE_PAGE_URL), + 'expected' => array('Page URL not defined'), + ), + array( + 'params' => array('name' => 'http://example.org/download.zip', 'type' => Action::TYPE_DOWNLOAD), + 'expected' => array('example.org', '/download.zip'), + ), + array( + 'params' => array('name' => 'http://example.org/download/1/', 'type' => Action::TYPE_DOWNLOAD), + 'expected' => array('example.org', '/download/1/'), + ), + array( + 'params' => array('name' => 'http://example.org/link', 'type' => Action::TYPE_OUTLINK), + 'expected' => array('example.org', '/link'), + ), + array( + 'params' => array('name' => 'http://example.org/some/path/', 'type' => Action::TYPE_OUTLINK), + 'expected' => array('example.org', '/some/path/'), + ), + ); + } + + /** + * @dataProvider getActionNameTestData + */ + public function testGetActionExplodedNames($params, $expected) + { + ArchivingHelper::reloadConfig(); + $processed = ArchivingHelper::getActionExplodedNames($params['name'], $params['type'], (isset($params['urlPrefix']) ? $params['urlPrefix'] : null)); + $this->assertEquals($expected, $processed); + } + + public function testDeleteInvalidSummedColumnsFromDataTableRemovesRenamedUniqMetricsFromParentRowsOnly() + { + $table = new DataTable(); + + $parentRow = new Row([Row::COLUMNS => [ + 'label' => '/parent', + PiwikMetrics::INDEX_NB_UNIQ_VISITORS => 12, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => 8, + PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => 4, + PiwikMetrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 20, + PiwikMetrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS => 16, + PiwikMetrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS => 14, + ]]); + $parentRow->setSubtable(new DataTable()); + $table->addRow($parentRow); + + $leafRow = new Row([Row::COLUMNS => [ + 'label' => '/leaf', + PiwikMetrics::INDEX_NB_UNIQ_VISITORS => 5, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => 4, + PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => 3, + PiwikMetrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 9, + PiwikMetrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS => 7, + PiwikMetrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS => 6, + ]]); + $table->addRow($leafRow); + + ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($table); + + $this->assertFalse($parentRow->hasColumn(PiwikMetrics::INDEX_NB_UNIQ_VISITORS)); + $this->assertFalse($parentRow->hasColumn(PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS)); + $this->assertFalse($parentRow->hasColumn(PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS)); + $this->assertFalse($parentRow->hasColumn(PiwikMetrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS)); + $this->assertFalse($parentRow->hasColumn(PiwikMetrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS)); + $this->assertFalse($parentRow->hasColumn(PiwikMetrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS)); + + $this->assertSame(9, $leafRow->getColumn(PiwikMetrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS)); + $this->assertSame(7, $leafRow->getColumn(PiwikMetrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS)); + $this->assertSame(6, $leafRow->getColumn(PiwikMetrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS)); + } + + public function testDeleteInvalidSummedColumnsFromDataTableKeepsRenamedUniqMetricsOnSummaryRow() + { + $table = new DataTable(); + $summaryRow = new Row([Row::COLUMNS => [ + 'label' => DataTable::LABEL_SUMMARY_ROW, + PiwikMetrics::INDEX_NB_UNIQ_VISITORS => 12, + PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => 8, + PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => 4, + PiwikMetrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 20, + PiwikMetrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS => 16, + PiwikMetrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS => 14, + ]]); + $table->addSummaryRow($summaryRow); + + ArchivingHelper::deleteInvalidSummedColumnsFromDataTable($table); + + $summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW); + $this->assertNotNull($summaryRow); + $this->assertFalse($summaryRow->hasColumn(PiwikMetrics::INDEX_NB_UNIQ_VISITORS)); + $this->assertFalse($summaryRow->hasColumn(PiwikMetrics::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS)); + $this->assertFalse($summaryRow->hasColumn(PiwikMetrics::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS)); + $this->assertSame(20, $summaryRow->getColumn(PiwikMetrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS)); + $this->assertSame(16, $summaryRow->getColumn(PiwikMetrics::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS)); + $this->assertSame(14, $summaryRow->getColumn(PiwikMetrics::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS)); + } +} diff --git a/plugins/Actions/tests/Unit/DataTableFilterActionsTest.php b/plugins/Actions/tests/Unit/DataTableFilterActionsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..478539ae2ef2fcde7b369fd2ea1f5cdaf2790ab6 --- /dev/null +++ b/plugins/Actions/tests/Unit/DataTableFilterActionsTest.php @@ -0,0 +1,174 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\tests\Unit; + +use Piwik\DataTable; +use Piwik\DataTable\Row; +use Piwik\Plugins\Actions\ArchivingHelper; +use Piwik\Plugins\Actions\DataTable\Filter\Actions as ActionsFilter; +use Piwik\Plugins\SitesManager\API as SitesManagerAPI; +use Piwik\Site; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tracker\Action; + +require_once PIWIK_INCLUDE_PATH . '/plugins/Actions/Actions.php'; + +/** + * @group Actions + * @group DataTableFilterActionsTest + * @group Plugins + */ +class DataTableFilterActionsTest extends \PHPUnit\Framework\TestCase +{ + public function setUp(): void + { + Fixture::loadAllTranslations(); + } + + public function tearDown(): void + { + Fixture::resetTranslations(); + SitesManagerAPI::unsetInstance(); + } + + public function testFilterSetsNullSegmentForUnknownPageUrlEvenWithoutSiteUrlPrefix() + { + ArchivingHelper::reloadConfig(); + + $table = new DataTable(); + $notDefinedUrl = ArchivingHelper::getUnknownActionName(Action::TYPE_PAGE_URL); + $row = new Row([Row::COLUMNS => ['label' => $notDefinedUrl]]); + $table->addRow($row); + + $filter = new ActionsFilter($table, Action::TYPE_PAGE_URL); + $filter->filter($table); + + $allMetadata = $row->getMetadata(); + $this->assertArrayHasKey('segment', $allMetadata); + $this->assertNull($allMetadata['segment']); + } + + public function testFilterEmitsOrJoinedSegmentForFolderRowOnSiteWithAliases() + { + ArchivingHelper::reloadConfig(); + + // Site stub: main_url + getId. + $site = $this->createMock(Site::class); + $site->method('getId')->willReturn(1); + $site->method('getMainUrl')->willReturn('https://main.example.com'); + + // SitesManager API stub returning main + one alias. + $sitesManager = $this->getMockBuilder(SitesManagerAPI::class) + ->disableOriginalConstructor() + ->onlyMethods(['getSiteUrlsFromId']) + ->getMock(); + $sitesManager->method('getSiteUrlsFromId')->willReturn([ + 'https://main.example.com', + 'https://alias.example.com', + ]); + SitesManagerAPI::setSingletonInstance($sitesManager); + + $table = new DataTable(); + $table->setMetadata('site', $site); + $row = new Row([Row::COLUMNS => ['label' => 'products']]); + $row->setMetadata('folder_url_start', 'https://main.example.com/products'); + $table->addRow($row); + + (new ActionsFilter($table, Action::TYPE_PAGE_URL))->filter($table); + + $segment = $row->getMetadata('segment'); + $this->assertIsString($segment); + + $mainClause = 'pageUrl=^' . urlencode(urlencode('https://main.example.com/products')); + $aliasClause = 'pageUrl=^' . urlencode(urlencode('https://alias.example.com/products')); + + $this->assertStringContainsString($mainClause, $segment); + $this->assertStringContainsString($aliasClause, $segment); + $this->assertSame($mainClause . ',' . $aliasClause, $segment); + } + + public function testFilterResolvesSiteUrlsOncePerIdSiteAcrossMultipleFolderRows() + { + ArchivingHelper::reloadConfig(); + + $site = $this->createMock(Site::class); + $site->method('getId')->willReturn(1); + $site->method('getMainUrl')->willReturn('https://main.example.com'); + + $sitesManager = $this->getMockBuilder(SitesManagerAPI::class) + ->disableOriginalConstructor() + ->onlyMethods(['getSiteUrlsFromId']) + ->getMock(); + $sitesManager->expects($this->once()) + ->method('getSiteUrlsFromId') + ->with(1) + ->willReturn([ + 'https://main.example.com', + 'https://alias.example.com', + ]); + SitesManagerAPI::setSingletonInstance($sitesManager); + + $table = new DataTable(); + $table->setMetadata('site', $site); + foreach (['products', 'blog', 'docs'] as $folder) { + $row = new Row([Row::COLUMNS => ['label' => $folder]]); + $row->setMetadata('folder_url_start', 'https://main.example.com/' . $folder); + $table->addRow($row); + } + + (new ActionsFilter($table, Action::TYPE_PAGE_URL))->filter($table); + } + + public function testFilterPreservesMainUrlPathPrefixWhenRebuildingFolderSegment() + { + ArchivingHelper::reloadConfig(); + + $site = $this->createMock(Site::class); + $site->method('getId')->willReturn(1); + $site->method('getMainUrl')->willReturn('https://example.com/section'); + + $sitesManager = $this->getMockBuilder(SitesManagerAPI::class) + ->disableOriginalConstructor() + ->onlyMethods(['getSiteUrlsFromId']) + ->getMock(); + $sitesManager->method('getSiteUrlsFromId')->willReturn([ + 'https://example.com/section', + 'https://alias.example.com/branch/', + ]); + SitesManagerAPI::setSingletonInstance($sitesManager); + + $table = new DataTable(); + $table->setMetadata('site', $site); + $row = new Row([Row::COLUMNS => ['label' => 'products']]); + $row->setMetadata('folder_url_start', 'https://example.com/section/products'); + $table->addRow($row); + + (new ActionsFilter($table, Action::TYPE_PAGE_URL))->filter($table); + + $mainClause = 'pageUrl=^' . urlencode(urlencode('https://example.com/section/products')); + $aliasClause = 'pageUrl=^' . urlencode(urlencode('https://alias.example.com/branch/products')); + $this->assertSame($mainClause . ',' . $aliasClause, $row->getMetadata('segment')); + } + + public function testFilterFallsBackToSingleClauseWhenSiteMetadataIsMissing() + { + ArchivingHelper::reloadConfig(); + + $table = new DataTable(); + $row = new Row([Row::COLUMNS => ['label' => 'products']]); + $row->setMetadata('folder_url_start', 'https://main.example.com/products'); + $table->addRow($row); + + (new ActionsFilter($table, Action::TYPE_PAGE_URL))->filter($table); + + $expected = 'pageUrl=^' . urlencode(urlencode('https://main.example.com/products')); + $this->assertSame($expected, $row->getMetadata('segment')); + } +} diff --git a/plugins/Actions/tests/Unit/Reports/BaseTest.php b/plugins/Actions/tests/Unit/Reports/BaseTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a32c39ef47dcb1dce0f4f1fcaba1777aad5f10f5 --- /dev/null +++ b/plugins/Actions/tests/Unit/Reports/BaseTest.php @@ -0,0 +1,70 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Actions\tests\Unit\Reports; + +use Piwik\Config; +use Piwik\Plugins\Actions\Reports\GetEntryPageTitles; +use Piwik\Plugins\Actions\Reports\GetPageTitles; +use Piwik\Plugins\Actions\Reports\GetPageUrls; + +class BaseTest extends \PHPUnit\Framework\TestCase +{ + public function testGetRecursiveLabelSeparatorUsesTitleDelimiterForPageTitleReports() + { + $this->withActionDelimiters('', ' :: ', '/', function () { + $this->assertSame(' :: ', (new GetPageTitles())->getRecursiveLabelSeparator()); + $this->assertSame(' :: ', (new GetEntryPageTitles())->getRecursiveLabelSeparator()); + }); + } + + public function testGetRecursiveLabelSeparatorUsesLegacyDelimiterWhenConfigured() + { + $this->withActionDelimiters(' > ', ' :: ', '/', function () { + $this->assertSame(' > ', (new GetPageTitles())->getRecursiveLabelSeparator()); + $this->assertSame(' > ', (new GetPageUrls())->getRecursiveLabelSeparator()); + }); + } + + private function withActionDelimiters($legacyDelimiter, $titleDelimiter, $urlDelimiter, callable $callback) + { + $config = Config::getInstance(); + + $keys = [ + 'action_category_delimiter', + 'action_title_category_delimiter', + 'action_url_category_delimiter', + ]; + + $previousValues = []; + foreach ($keys as $key) { + $hadValue = array_key_exists($key, $config->General); + $previousValues[$key] = [ + 'hadValue' => $hadValue, + 'value' => $hadValue ? $config->General[$key] : null, + ]; + } + + $config->General['action_category_delimiter'] = $legacyDelimiter; + $config->General['action_title_category_delimiter'] = $titleDelimiter; + $config->General['action_url_category_delimiter'] = $urlDelimiter; + + try { + $callback(); + } finally { + foreach ($previousValues as $key => $previousValue) { + if ($previousValue['hadValue']) { + $config->General[$key] = $previousValue['value']; + } else { + unset($config->General[$key]); + } + } + } + } +} diff --git a/plugins/Annotations/API.php b/plugins/Annotations/API.php new file mode 100644 index 0000000000000000000000000000000000000000..f67d867d70442f0e92413d4a95cd963a9c896bc6 --- /dev/null +++ b/plugins/Annotations/API.php @@ -0,0 +1,348 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Annotations; + +use Exception; +use Piwik\Common; +use Piwik\Date; +use Piwik\Piwik; +use Piwik\Site; + +/** + * Provides API methods to create, update, delete, and query annotations. + * + * @phpstan-type Annotation array{id:int, idNote:int, idsite:int, date:string, note:string, starred:int, user:string, canEditOrDelete:bool} + * + * @method static \Piwik\Plugins\Annotations\API getInstance() + */ +class API extends \Piwik\Plugin\API +{ + // do not automatically apply `Common::sanitizeInputValue` to all API parameters + protected $autoSanitizeInputParams = false; + + /** + * Creates a new annotation for a site. + * + * @param int $idSite The site ID to add the annotation to. + * @param string $date The date the annotation is attached to. + * @param string $note The text of the annotation (max 255 chars). + * @param bool $starred Whether the annotation should be starred. + * @return Annotation + */ + public function add(int $idSite, string $date, string $note, bool $starred = false): array + { + $this->checkUserCanAddNotesFor($idSite); + $this->checkSiteExists($idSite); + $this->checkDateIsValid($date); + $date = Date::factory($date)->toString(); // ensure we handle today/yesterday correctly + + $annotation = [ + 'idsite' => $idSite, + 'date' => $date, + 'note' => $this->filterNote($note), + 'starred' => (int) $starred, + 'user' => Piwik::getCurrentUserLogin(), + ]; + + $model = new Model(); + $idNote = $model->createAnnotation($annotation); + $annotation['id'] = $idNote; + $this->decorateAnnotation($annotation); + + return $annotation; + } + + /** + * Updates an annotation for a site and returns the updated annotation. + * + * If the current user is not allowed to modify an annotation, an exception + * will be thrown. A user can modify a note if: + * - the user has admin access for the site, OR + * - the user has view access, is not the anonymous user and is the user that + * created the note + * + * @param int $idSite The site ID to add the annotation to. + * @param int $idNote The ID of the note. + * @param string|null $date The date the annotation is attached to. If null, the annotation's + * date is not modified. + * @param string|null $note The text of the annotation (max 255 chars). + * If null, the annotation's text is not modified. + * @param bool|null $starred Whether the annotation should be starred. + * If null, the annotation is not starred/un-starred, so the current state won't change. + * @return Annotation + */ + public function save(int $idSite, int $idNote, ?string $date = null, ?string $note = null, ?bool $starred = null): array + { + $this->checkSiteExists($idSite); + $this->checkDateIsValid($date, $canBeNull = true); + if (null !== $date) { + $date = Date::factory($date)->toString(); // ensure we handle today/yesterday correctly + } + + $originalAnnotation = $this->get($idSite, $idNote); + + // check if current user has the right to update the annotation + $this->checkUserCanModifyOrDelete($originalAnnotation); + + if (isset($starred)) { + $starred = intval($starred); + } + + $updatedValues = array_diff_assoc( + array_filter( + [ + 'date' => $date, + 'note' => $this->filterNote($note), + 'starred' => $starred, + ], + function ($value) { + return isset($value); + } + ), + $originalAnnotation + ); + + if (!empty($updatedValues)) { + $model = new Model(); + $originalAnnotation = $model->updateAnnotation($idNote, $idSite, $updatedValues); + } + $this->decorateAnnotation($originalAnnotation); + + return $originalAnnotation; + } + + /** + * Removes an annotation from a site's list of annotations. + * + * If the current user is not allowed to delete the annotation, an exception + * will be thrown. A user can delete a note if: + * - the user has admin access for the site, OR + * - the user has view access, is not the anonymous user and is the user that + * created the note + * + * @param int $idSite The site ID to add the annotation to. + * @param int $idNote The ID of the note to delete. + */ + public function delete(int $idSite, int $idNote): void + { + $this->checkSiteExists($idSite); + + $annotation = $this->get($idSite, $idNote); + // check permissions + $this->checkUserCanModifyOrDelete($annotation); + + $model = new Model(); + $model->deleteAnnotation($idNote, $idSite); + } + + /** + * Removes all annotations for a single site. Only superusers can use this method. + * + * @param int $idSite The ID of the site to remove annotations for. + */ + public function deleteAll(int $idSite): void + { + Piwik::checkUserHasSuperUserAccess(); + + $this->checkSiteExists($idSite); + + $model = new Model(); + $model->deleteAllAnnotationsForSite($idSite); + } + + /** + * Returns a single annotation for one site. + * + * @param int $idSite The site ID the annotation is linked to. + * @param int $idNote The ID of the annotation to get. + * @return Annotation + */ + public function get(int $idSite, int $idNote): array + { + Piwik::checkUserHasViewAccess($idSite); + $this->checkSiteExists($idSite); + + $model = new Model(); + $annotation = $model->getAnnotation($idNote, $idSite); + if (empty($annotation)) { + throw new Exception("There is no note with id '$idNote' for site with id '$idSite'."); + } + $this->decorateAnnotation($annotation); + + return $annotation; + } + + /** + * Returns every annotation for a specific site within a date range. + * The date range is derived from a date, period, and optional number of past periods. + * + * @param string $idSite One site ID or a comma-separated list of site IDs. + * @param string|null $date The date of the period. + * @param 'day'|'week'|'month'|'year'|'range' $period The period type. + * @param int|null $lastN Whether to include the last N periods in the date range. + * @return array<int, array<int, Annotation>> + */ + public function getAll(string $idSite, ?string $date = null, string $period = 'day', ?int $lastN = null): array + { + Piwik::checkUserHasViewAccess($idSite); + + $ids = array_map('intval', Site::getIdSitesFromIdSitesString($idSite, false, true)); + $model = new Model(); + $annotations = []; + foreach ($ids as $id) { + [$startDate, $endDate] = Annotations::getDateRangeForPeriod($date ?: false, $period, $lastN ?? false, $id); + + $annotations[$id] = $model->getAllAnnotationsForSiteInRange( + $id, + $startDate ? $startDate->toString() : null, + $endDate ? $endDate->toString() : null + ); + for ($i = 0; $i < count($annotations[$id]); $i++) { + $this->decorateAnnotation($annotations[$id][$i]); + } + } + + return $annotations; + } + + /** + * Returns the count of annotations for a list of periods, including the count of + * starred annotations. + * + * @param string $idSite The site ID(s) to get the annotation count for. + * @param string $date The date of the period. + * @param 'day'|'week'|'month'|'year'|'range' $period The period type. + * @param int|null $lastN Whether to get counts for the last N number of periods or not. + * @param bool $getAnnotationText Whether to include the note text when exactly one annotation exists for a date. + * @return array<int, array<int, array{0:string, 1:array{count:int, starred:int, note?:string}}>> + */ + public function getAnnotationCountForDates( + string $idSite, + string $date, + string $period, + ?int $lastN = null, + bool $getAnnotationText = false + ): array { + Piwik::checkUserHasViewAccess($idSite); + + $siteIds = array_map('intval', Site::getIdSitesFromIdSitesString($idSite, false, true)); + if (empty($siteIds)) { + return []; + } + + // get start & end date for request. lastN is ignored if $period == 'range' + [$startDate, $endDate] = Annotations::getDateRangeForPeriod($date, $period, $lastN ?? false, $siteIds[0]); + + if (!($startDate && $endDate)) { + return []; + } + + if ($period == 'range') { + $period = 'day'; + } + + // create list of dates + $dates = []; + for (; $startDate->getTimestamp() <= $endDate->getTimestamp(); $startDate = $startDate->addPeriod(1, $period)) { + $dates[] = $startDate; + } + // we add one for the end of the last period (used in for loop below to bound annotation dates) + $dates[] = $startDate; + + $model = new Model(); + $result = []; + foreach ($siteIds as $siteId) { + $result[$siteId] = []; + for ($i = 0; $i < count($dates) - 1; $i++) { + $date = $dates[$i]; + $nextDate = $dates[$i + 1]; + $strDate = $date->toString(); + $strNextDate = $nextDate->toString(); + + [$totalCount, $starredCount] = $model->getCountAnnotationsForSiteInRange($siteId, $strDate, $strNextDate); + + $result[$siteId][] = [ + $strDate, + [ + 'count' => $totalCount, + 'starred' => $starredCount, + ], + ]; + + if ($getAnnotationText && $totalCount === 1) { + [$annotation] = $model->getAllAnnotationsForSiteInRange($siteId, $strDate, $strNextDate, 1); + // 1 for the second array to add the note to + $result[$siteId][$i][1]['note'] = (string)$annotation['note']; + } + } + } + + return $result; + } + + /** + * @param array{id:int, idsite:int, date:string, note:string, starred:int, user:string} $annotation + */ + private function checkUserCanModifyOrDelete(array $annotation): void + { + if (!Annotations::canUserModifyOrDelete($annotation)) { + throw new Exception("The current user is not allowed to modify or delete notes for site #{$annotation['idsite']}"); + } + } + + private static function checkUserCanAddNotesFor(int $idSite): void + { + if (!Piwik::isUserHasViewAccess($idSite) || Piwik::isUserIsAnonymous()) { + throw new Exception("The current user is not allowed to add notes for site #$idSite."); + } + } + + private function checkSiteExists(int $idSite): void + { + new Site($idSite); + } + + private function checkDateIsValid(?string $date, bool $canBeNull = false): void + { + if ( + $date === null + && $canBeNull + ) { + return; + } + + Date::factory($date); + } + + private function filterNote(?string $note): ?string + { + if (empty($note)) { + return $note; + } + + // shorten note if longer than 255 characters + if (mb_strlen($note) > 255) { + $note = mb_substr($note, 0, 254) . '…'; + } + + return $note; + } + + /** + * @param array{id:int, idsite:int, date:string, note:string, starred:int, user:string} $annotation + */ + private function decorateAnnotation(array &$annotation): void + { + $annotation['date'] = substr($annotation['date'], 0, 10); + $annotation['note'] = Common::sanitizeInputValue($annotation['note']); + $annotation['canEditOrDelete'] = Annotations::canUserModifyOrDelete($annotation); + $annotation['idNote'] = $annotation['id']; // for API backward compatibility + } +} diff --git a/plugins/Annotations/Annotations.php b/plugins/Annotations/Annotations.php new file mode 100644 index 0000000000000000000000000000000000000000..ad3e1869e27d3e0c29bcb0dd315faceb4cd15339 --- /dev/null +++ b/plugins/Annotations/Annotations.php @@ -0,0 +1,128 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Annotations; + +use Piwik\Date; +use Piwik\Period; +use Piwik\Piwik; +use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz; + +/** + * Annotations plugins. Provides the ability to attach text notes to + * dates for each site. Notes can be viewed, modified, deleted or starred. + * + */ +class Annotations extends \Piwik\Plugin +{ + /** + * @see \Piwik\Plugin::registerEvents + */ + public function registerEvents() + { + return array( + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'AssetManager.getJavaScriptFiles' => 'getJsFiles', + 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + ); + } + + public function getClientSideTranslationKeys(&$translationKeys) + { + $translationKeys[] = 'Intl_Today'; + } + + /** + * Adds css files for this plugin to the list in the event notification. + */ + public function getStylesheetFiles(&$stylesheets) + { + $stylesheets[] = "plugins/Annotations/stylesheets/annotations.less"; + } + + /** + * Adds js files for this plugin to the list in the event notification. + */ + public function getJsFiles(&$jsFiles) + { + $jsFiles[] = "plugins/Annotations/javascripts/annotations.js"; + } + + + /** + * Returns start & end dates for the range described by a period and optional lastN + * argument. + * + * @param string|bool $date The start date of the period (or the date range of a range + * period). + * @param string $period The period type ('day', 'week', 'month', 'year' or 'range'). + * @param bool|int $lastN Whether to include the last N periods in the range or not. + * Ignored if period == range. + * @param int|null $idSite the id of the current site, used to get timezone + * + * @return array{0: Date, 1: Date}|array{0: false, 1: false} + */ + public static function getDateRangeForPeriod($date, $period, $lastN = false, ?int $idSite = null): array + { + if ($date === false) { + return [false, false]; + } + + $isMultiplePeriod = Period\Range::isMultiplePeriod($date, $period); + + // if the range is just a normal period (or the period is a range in which case lastN is ignored) + if ($period == 'range') { + $oPeriod = new Period\Range('day', $date); + $startDate = $oPeriod->getDateStart()->getStartOfDay(); + $endDate = $oPeriod->getDateEnd()->getStartOfDay(); + } elseif ($lastN == false && !$isMultiplePeriod) { + $oPeriod = Period\Factory::build($period, Date::factory($date)); + $startDate = $oPeriod->getDateStart(); + $endDate = $oPeriod->getDateEnd(); + } else { // if the range includes the last N periods or is a multiple period + if (!$isMultiplePeriod) { + [$date, $lastN] = EvolutionViz::getDateRangeAndLastN($period, $date, $lastN, $idSite); + } + [$startDate, $endDate] = explode(',', $date); + + $startDate = Date::factory($startDate); + $endDate = Date::factory($endDate); + } + + // Limit the end date to the maximum allowed range end to prevent generating huge date ranges. + // Date::factory() already rejects dates before the first website timestamp, so only the (future) + // end of the range needs to be clamped here. + $maxAllowedEndDate = Date::factory(Period\Range::getMaxAllowedEndTimestamp()); + if ($endDate->isLater($maxAllowedEndDate)) { + $endDate = $maxAllowedEndDate; + } + if ($startDate->isLater($endDate)) { + $startDate = $endDate; + } + + return [$startDate, $endDate]; + } + + /** + * Returns true if the current user can modify or delete a specific annotation. + * + * A user can modify/delete a note if the user has write access for the site OR + * the user has view access, is not the anonymous user and is the user that + * created the note in question. + * + * @param array $annotation The annotation. + */ + public static function canUserModifyOrDelete(array $annotation): bool + { + // user can save if user is admin or if has view access, is not anonymous & is user who wrote note + return Piwik::isUserHasWriteAccess($annotation['idsite']) + || (!Piwik::isUserIsAnonymous() + && Piwik::getCurrentUserLogin() === $annotation['user']); + } +} diff --git a/plugins/Annotations/Controller.php b/plugins/Annotations/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..c06684917a7512e7975f90579c28b30bded04c46 --- /dev/null +++ b/plugins/Annotations/Controller.php @@ -0,0 +1,237 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Annotations; + +use Piwik\API\Request; +use Piwik\Common; +use Piwik\Date; +use Piwik\Piwik; +use Piwik\View; + +/** + * Controller for the Annotations plugin. + * + */ +class Controller extends \Piwik\Plugin\Controller +{ + /** + * Controller action that returns HTML displaying annotations for a site and + * specific date range. + * + * Query Param Input: + * - idSite: The ID of the site to get annotations for. Only one allowed. + * - date: The date to get annotations for. If lastN is not supplied, this is the start date, + * otherwise the start date in the last period. + * - period: The period type. + * - lastN: If supplied, the last N # of periods will be included w/ the range specified + * by date + period. + * + * Output: + * - HTML displaying annotations for a specific range. + * + * @param bool $fetch True if the annotation manager should be returned as a string, + * false if it should be echo-ed. + * @param bool|string $date Override for 'date' query parameter. + * @param bool|string $period Override for 'period' query parameter. + * @param bool|string $lastN Override for 'lastN' query parameter. + * @return string|void + */ + public function getAnnotationManager($fetch = false, $date = false, $period = false, $lastN = false) + { + $this->checkSitePermission(); + + if ($date === false) { + $date = Common::getRequestVar('date', false); + } + + if ($period === false) { + $period = Common::getRequestVar('period', 'day'); + } + + if ($lastN === false) { + $lastN = Common::getRequestVar('lastN', false); + } + + // create & render the view + $view = new View('@Annotations/getAnnotationManager'); + + $allAnnotations = Request::processRequest( + 'Annotations.getAll', + array('date' => $date, 'period' => $period, 'lastN' => $lastN) + ); + $view->annotations = empty($allAnnotations[$this->idSite]) ? array() : $allAnnotations[$this->idSite]; + + $view->period = $period; + $view->lastN = $lastN; + + list($startDate, $endDate) = Annotations::getDateRangeForPeriod($date, $period, $lastN); + $view->startDate = $startDate->toString(); + $view->endDate = $endDate->toString(); + + if ($startDate->toString() !== $endDate->toString()) { + $view->selectedDate = Date::today()->toString(); + } else { + $view->selectedDate = $endDate->toString(); + } + + $dateFormat = Date::DATE_FORMAT_SHORT; + $view->startDatePretty = $startDate->getLocalized($dateFormat); + $view->endDatePretty = $endDate->getLocalized($dateFormat); + + $view->canUserAddNotes = self::canUserAddNotesFor($this->idSite); + + return $view->render(); + } + + /** + * Controller action that modifies an annotation and returns HTML displaying + * the modified annotation. + * + * Query Param Input: + * - idSite: The ID of the site the annotation belongs to. Only one ID is allowed. + * - idNote: The ID of the annotation. + * - date: The new date value for the annotation. (optional) + * - note: The new text for the annotation. (optional) + * - starred: Either 1 or 0. Whether the note should be starred or not. (optional) + * + * Output: + * - HTML displaying modified annotation. + * + * If an optional query param is not supplied, that part of the annotation is + * not modified. + */ + public function saveAnnotation() + { + if ($_SERVER["REQUEST_METHOD"] == "POST") { + $this->checkTokenInUrl(); + + $view = new View('@Annotations/saveAnnotation'); + + // NOTE: permissions checked in API method + // save the annotation + $view->annotation = Request::processRequest("Annotations.save"); + + return $view->render(); + } + } + + /** + * Controller action that adds a new annotation for a site and returns new + * annotation manager HTML for the site and date range. + * + * Query Param Input: + * - idSite: The ID of the site to add an annotation to. + * - date: The date for the new annotation. + * - note: The text of the annotation. + * - starred: Either 1 or 0, whether the annotation should be starred or not. + * Defaults to 0. + * - managerDate: The date for the annotation manager. If a range is given, the start + * date is used for the new annotation. + * - managerPeriod: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * - lastN: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * Output: + * - @see self::getAnnotationManager + */ + public function addAnnotation() + { + if ($_SERVER["REQUEST_METHOD"] == "POST") { + $this->checkTokenInUrl(); + + // the date used is for the annotation manager HTML that gets echo'd. we + // use this date for the new annotation, unless it is a date range, in + // which case we use the first date of the range. + $date = Common::getRequestVar('date'); + if (strpos($date, ',') !== false) { + $date = reset(explode(',', $date)); + } + + // add the annotation. NOTE: permissions checked in API method + Request::processRequest("Annotations.add", array('date' => $date)); + + $managerDate = Common::getRequestVar('managerDate', false); + $managerPeriod = Common::getRequestVar('managerPeriod', false); + return $this->getAnnotationManager($fetch = true, $managerDate, $managerPeriod); + } + } + + /** + * Controller action that deletes an annotation and returns new annotation + * manager HTML for the site & date range. + * + * Query Param Input: + * - idSite: The ID of the site this annotation belongs to. + * - idNote: The ID of the annotation to delete. + * - date: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * - period: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * - lastN: For rendering the annotation manager. @see self::getAnnotationManager + * for more info. + * + * Output: + * - @see self::getAnnotationManager + */ + public function deleteAnnotation() + { + if ($_SERVER["REQUEST_METHOD"] == "POST") { + $this->checkTokenInUrl(); + + // delete annotation. NOTE: permissions checked in API method + Request::processRequest("Annotations.delete"); + + return $this->getAnnotationManager($fetch = true); + } + } + + /** + * Controller action that echo's HTML that displays marker icons for an + * evolution graph's x-axis. The marker icons still need to be positioned + * by the JavaScript. + * + * Query Param Input: + * - idSite: The ID of the site this annotation belongs to. Only one is allowed. + * - date: The date to check for annotations. If lastN is not supplied, this is + * the start of the date range used to check for annotations. If supplied, + * this is the start of the last period in the date range. + * - period: The period type. + * - lastN: If supplied, the last N # of periods are included in the date range + * used to check for annotations. + * + * Output: + * - HTML that displays marker icons for an evolution graph based on the + * number of annotations & starred annotations in the graph's date range. + */ + public function getEvolutionIcons() + { + // get annotation the count + $annotationCounts = Request::processRequest( + "Annotations.getAnnotationCountForDates", + array('getAnnotationText' => 1) + ); + + // create & render the view + $view = new View('@Annotations/getEvolutionIcons'); + $view->annotationCounts = reset($annotationCounts); // only one idSite allowed for this action + + return $view->render(); + } + + /** + * Returns true if the current user can add notes for a specific site. + * + * @param int $idSite The site to add notes to. + */ + public static function canUserAddNotesFor(int $idSite): bool + { + return Piwik::isUserHasWriteAccess($idSite); + } +} diff --git a/plugins/Annotations/Model.php b/plugins/Annotations/Model.php new file mode 100644 index 0000000000000000000000000000000000000000..b471e2007db572b9aa27f1f035f3f111eef258f2 --- /dev/null +++ b/plugins/Annotations/Model.php @@ -0,0 +1,152 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Annotations; + +use Piwik\Common; +use Piwik\Db; + +/** + * @phpstan-type Annotation array{id:int, idsite:int, date:string, note:string, starred:int, user:string} + */ +class Model +{ + private const TABLE_NAME = 'annotations'; + private const EDITABLE_COLS = ['note', 'date', 'starred']; + /** @var string */ + private $table; + + public function __construct() + { + $this->table = Common::prefixTable(self::TABLE_NAME); + } + + /** + * @param array<string,mixed> $annotation + */ + public function createAnnotation(array $annotation): int + { + $db = $this->getDb(); + $db->insert($this->table, $annotation); + + return (int) $db->lastInsertId(); + } + + /** + * @return Annotation|array{} + */ + public function getAnnotation(int $annotationId, int $idSite): array + { + $db = $this->getDb(); + $query = "SELECT * FROM $this->table WHERE id = ? AND idsite = ?"; + $bind = [$annotationId, $idSite]; + + return $db->fetchRow($query, $bind) ?: []; + } + + /** + * @return list<Annotation> + */ + public function getAllAnnotationsForSiteInRange(int $idSite, ?string $startDate = null, ?string $endDate = null, ?int $limit = null): array + { + $db = $this->getDb(); + $query = "SELECT * FROM $this->table WHERE idsite = ?"; + $bind = [ + $idSite, + ]; + + if (null !== $startDate) { + $query .= " AND date >= ?"; + $bind[] = $startDate; + } + if (null !== $endDate) { + $query .= " AND date <= ?"; + $bind[] = $endDate; + } + if (null !== $limit) { + $query .= " LIMIT $limit"; + } + + return $db->fetchAll($query, $bind); + } + + /** + * @return array{int, int} + */ + public function getCountAnnotationsForSiteInRange(int $idSite, string $startDate, string $endDate): array + { + $db = $this->getDb(); + $query = "SELECT + SUM(1) AS cnt_total, + SUM(CASE WHEN starred = 1 THEN 1 ELSE 0 END) AS cnt_starred + FROM $this->table + WHERE idsite = ? AND date >= ? AND date < ?"; + $bind = [ + $idSite, + $startDate, + $endDate, + ]; + $result = $db->fetchRow($query, $bind); + + return [intval($result['cnt_total'] ?? 0), intval($result['cnt_starred'] ?? 0)]; + } + + /** + * Update existing annotation with provided data + * + * @param int $annotationId id of the annotation being updated + * @param int $idSite the site of the annotation + * @param array<string,string|int|bool> $updatedColumns an associative array containing columns to update, + * only columns matching $this->getEditableColumns() are used. + * @return Annotation the updated annotation + */ + public function updateAnnotation(int $annotationId, int $idSite, array $updatedColumns): array + { + $db = $this->getDb(); + $query = "UPDATE $this->table SET"; + $bind = []; + foreach (self::EDITABLE_COLS as $columnName) { + if (isset($updatedColumns[$columnName])) { + $query .= " $columnName = ?,"; + $bind[] = $updatedColumns[$columnName]; + } + } + $query = rtrim($query, ','); + $query .= " WHERE id = ? AND idsite = ?"; + $bind[] = $annotationId; + $bind[] = $idSite; + $db->query($query, $bind); + + return $this->getAnnotation($annotationId, $idSite); + } + + public function deleteAnnotation(int $annotationId, int $idsite): void + { + $db = $this->getDb(); + $query = "DELETE FROM $this->table WHERE id = ? AND idsite = ?"; + $bind = [$annotationId, $idsite]; + $db->query($query, $bind); + } + + public function deleteAllAnnotationsForSite(int $idSite): void + { + $db = $this->getDb(); + $query = "DELETE FROM $this->table WHERE idsite = ?"; + $bind = [$idSite]; + $db->query($query, $bind); + } + + /** + * @return Db|Db\AdapterInterface|\Zend_Db_Adapter_Abstract + */ + private function getDb() + { + return Db::get(); + } +} diff --git a/plugins/Annotations/config/config.php b/plugins/Annotations/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/Annotations/config/config.php @@ -0,0 +1,3 @@ +<?php + +return array(); diff --git a/plugins/Annotations/config/tracker.php b/plugins/Annotations/config/tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..d5be5de26f92a935f318ed4c0a04fe01434bfe66 --- /dev/null +++ b/plugins/Annotations/config/tracker.php @@ -0,0 +1,3 @@ +<?php + +return array(); diff --git a/plugins/Annotations/javascripts/annotations.js b/plugins/Annotations/javascripts/annotations.js new file mode 100644 index 0000000000000000000000000000000000000000..1b6c5f2927a99421fd0dedfdb3db203b1e6d671e --- /dev/null +++ b/plugins/Annotations/javascripts/annotations.js @@ -0,0 +1,637 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +(function ($, piwik) { + + var annotationsApi = { + + // calls Annotations.getAnnotationManager + getAnnotationManager: function (idSite, date, period, lastN, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'getAnnotationManager', + idSite: idSite, + date: date, + period: period, + filter_limit: '-1' + }; + if (lastN) { + ajaxParams.lastN = lastN; + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + ajaxRequest.setCallback(callback); + ajaxRequest.setFormat('html'); + ajaxRequest.send(); + }, + + // calls Annotations.addAnnotation + addAnnotation: function (idSite, managerDate, managerPeriod, date, note, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'addAnnotation', + idSite: idSite, + date: date, + managerDate: managerDate, + managerPeriod: managerPeriod, + note: note + }; + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + // Do not need to include the period parameter as this is not used in the endpoint + ajaxRequest.removeDefaultParameter('period'); + ajaxRequest.withTokenInUrl(); + ajaxRequest.setCallback(callback); + ajaxRequest.setFormat('html'); + ajaxRequest.send(); + }, + + // calls Annotations.saveAnnotation + saveAnnotation: function (idSite, idNote, date, noteData, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'saveAnnotation', + idSite: idSite, + idNote: idNote, + date: date + }; + + for (var key in noteData) { + ajaxParams[key] = noteData[key]; + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + // Do not need to include the period parameter as this is not used in the endpoint + ajaxRequest.removeDefaultParameter('period'); + ajaxRequest.withTokenInUrl(); + ajaxRequest.setCallback(callback); + ajaxRequest.setFormat('html'); + ajaxRequest.send(); + }, + + // calls Annotations.deleteAnnotation + deleteAnnotation: function (idSite, idNote, managerDate, managerPeriod, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'deleteAnnotation', + idSite: idSite, + idNote: idNote, + date: managerDate, + period: managerPeriod + }; + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + ajaxRequest.withTokenInUrl(); + ajaxRequest.setCallback(callback); + ajaxRequest.setFormat('html'); + ajaxRequest.send(); + }, + + // calls Annotations.getEvolutionIcons + getEvolutionIcons: function (idSite, date, period, lastN, callback) { + var ajaxParams = + { + module: 'Annotations', + action: 'getEvolutionIcons', + idSite: idSite, + date: date, + period: period, + filter_limit: '-1' + }; + if (lastN) { + ajaxParams.lastN = lastN; + } + + var ajaxRequest = new ajaxHelper(); + ajaxRequest.addParams(ajaxParams, 'get'); + ajaxRequest.withTokenInUrl(); + ajaxRequest.setFormat('html'); + ajaxRequest.setCallback(callback); + ajaxRequest.send(); + } + }; + + var today = new Date(); + + /** + * Returns options to configure an annotation's datepicker shown in edit mode. + * + * @param {Element} annotation The annotation element. + */ + var getDatePickerOptions = function (annotation) { + var annotationDateStr = annotation.attr('data-date'), + parts = annotationDateStr.split('-'), + annotationDate = new Date(parts[0], parts[1] - 1, parts[2]); + + var result = piwik.getBaseDatePickerOptions(annotationDate); + result.showButtonPanel = true; + result.currentText = _pk_translate('Intl_Today'); + + // make sure days before site start & after today cannot be selected + var piwikMinDate = result.minDate; + result.beforeShowDay = function (date) { + var valid = true; + + // if date is after today or before date of site creation, it cannot be selected + if (date > today + || date < piwikMinDate) { + valid = false; + } + + return [valid, '']; + }; + + // on select a date, change the text of the edit date link + result.onSelect = function (dateText) { + $('.annotation-period-edit>a', annotation).text(dateText); + $('.datepicker', annotation).hide(); + }; + + return result; + }; + + /** + * Switches the current mode of an annotation between the view/edit modes. + * + * @param {Element} inAnnotationElement An element within the annotation to toggle the mode of. + * Should be two levels nested in the .annotation-value + * element. + * @return {Element} The .annotation-value element. + */ + var toggleAnnotationMode = function (inAnnotationElement) { + var annotation = $(inAnnotationElement).closest('.annotation'); + annotation.toggleClass('edit') + $('.annotation-period,.annotation-period-edit,.delete-annotation,' + + '.annotation-edit-mode,.annotation-view-mode', annotation).toggle(); + + return $(inAnnotationElement).find('.annotation-value'); + }; + + /** + * Creates the datepicker for an annotation element. + * + * @param {Element} annotation The annotation element. + */ + var createDatePicker = function (annotation) { + $('.datepicker', annotation).datepicker(getDatePickerOptions(annotation)).hide(); + }; + + /** + * Creates datepickers for every period edit in an annotation manager. + * + * @param {Element} manager The annotation manager element. + */ + var createDatePickers = function (manager) { + $('.annotation-period-edit', manager).each(function () { + createDatePicker($(this).parent().parent()); + }); + }; + + /** + * Replaces the HTML of an annotation manager element, and resets date/period + * attributes. + * + * @param {Element} manager The annotation manager. + * @param {string} html The HTML of the new annotation manager. + */ + var replaceAnnotationManager = function (manager, html) { + var newManager = $(html); + manager.html(newManager.html()) + .attr('data-date', newManager.attr('data-date')) + .attr('data-period', newManager.attr('data-period')); + createDatePickers(manager); + }; + + /** + * Returns true if an annotation element is starred, false if otherwise. + * + * @param {Element} annotation The annotation element. + * @return {boolean} + */ + var isAnnotationStarred = function (annotation) { + return !!(+$('.annotation-star', annotation).attr('data-starred') == 1); + }; + + /** + * Replaces the HTML of an annotation element with HTML returned from Piwik, and + * makes sure the data attributes are correct. + * + * @param {Element} annotation The annotation element. + * @param {string} html The replacement HTML (or alternatively, the replacement + * element/jQuery object). + */ + var replaceAnnotationHtml = function (annotation, html) { + var newHtml = $(html); + annotation.html(newHtml.html()).attr('data-date', newHtml.attr('data-date')); + createDatePicker(annotation); + }; + + /** + * Binds events to an annotation manager element. + * + * @param {Element} manager The annotation manager. + * @param {int} idSite The site ID the manager is showing annotations for. + * @param {function} onAnnotationCountChange Callback that is called when there is a change + * in the number of annotations and/or starred annotations, + * eg, when a user adds a new one or deletes an existing one. + */ + var bindAnnotationManagerEvents = function (manager, idSite, onAnnotationCountChange) { + if (!onAnnotationCountChange) { + onAnnotationCountChange = function () {}; + } + + // show new annotation row if create new annotation link is clicked + manager.on('click', '.add-annotation', function (e) { + e.preventDefault(); + + var $newRow = $('.new-annotation-row', manager); + $newRow.show(); + $(this).hide(); + + return false; + }); + + // hide new annotation row if cancel button clicked + manager.on('click', '.new-annotation-cancel', function () { + var newAnnotationRow = $(this).parent().parent(); + newAnnotationRow.hide(); + + $('.add-annotation', newAnnotationRow.closest('.annotation-manager')).show(); + }); + + // save new annotation when new annotation row save is clicked + manager.on('click', '.new-annotation-save', function () { + var addRow = $(this).parent().parent(), + addNoteInput = addRow.find('.new-annotation-edit'), + noteDate = addRow.find('.annotation-period-edit>a').text(); + + // do nothing if input is empty + if (!addNoteInput.val()) { + return; + } + + // disable input & link + addNoteInput.attr('disabled', 'disabled'); + $(this).attr('disabled', 'disabled'); + + // add a new annotation for the site, date & period + annotationsApi.addAnnotation( + idSite, + manager.attr('data-date'), + manager.attr('data-period'), + noteDate, + addNoteInput.val(), + function (response) { + replaceAnnotationManager(manager, response); + + // increment annotation count for this date + onAnnotationCountChange(noteDate, 1, 0); + } + ); + }); + + // add new annotation when enter key pressed on new annotation input + manager.on('keypress', '.new-annotation-edit', function (e) { + if (e.which == 13) { + $(this).parent().find('.new-annotation-save').click(); + } + }); + + // show annotation editor if edit link, annotation text or period text is clicked + manager.on('click', '.annotation-enter-edit-mode', function (e) { + e.preventDefault(); + + var annotationContent = toggleAnnotationMode(this); + annotationContent.find('.annotation-edit').focus(); + + return false; + }); + + // hide annotation editor if cancel button is clicked + manager.on('click', '.annotation-cancel', function () { + toggleAnnotationMode(this); + }); + + // save annotation if save button clicked + manager.on('click', '.annotation-edit-mode .annotation-save', function () { + var annotation = $(this).parent().parent().parent(), + input = $('.annotation-edit', annotation), + dateEditText = $('.annotation-period-edit>a', annotation).text(); + + // if annotation value/date has not changed, just show the view mode instead of edit + if (input[0].defaultValue == input.val() + && dateEditText == annotation.attr('data-date')) { + toggleAnnotationMode(this); + return; + } + + // disable input while ajax is happening + input.attr('disabled', 'disabled'); + $(this).attr('disabled', 'disabled'); + + // save the note w/ the new note text & date + annotationsApi.saveAnnotation( + idSite, + annotation.attr('data-id'), + dateEditText, + { + note: input.val() + }, + function (response) { + response = $(response); + + var newDate = response.attr('data-date'), + isStarred = isAnnotationStarred(response), + originalDate = annotation.attr('data-date'); + + replaceAnnotationHtml(annotation, response); + + // if the date has been changed, update the evolution icon counts to reflect the change + if (originalDate != newDate) { + // reduce count for original date + onAnnotationCountChange(originalDate, -1, isStarred ? -1 : 0); + + // increase count for new date + onAnnotationCountChange(newDate, 1, isStarred ? 1 : 0); + } + } + ); + }); + + // save annotation if 'enter' pressed on input + manager.on('keypress', '.annotation-value input', function (e) { + if (e.which == 13) { + $(this).parent().find('.annotation-save').click(); + } + }); + + // delete annotation if delete link clicked + manager.on('click', '.delete-annotation', function (e) { + e.preventDefault(); + + var annotation = $(this).parent().parent(); + $(this).attr('disabled', 'disabled'); + + // delete annotation by ajax + annotationsApi.deleteAnnotation( + idSite, + annotation.attr('data-id'), + manager.attr('data-date'), + manager.attr('data-period'), + function (response) { + replaceAnnotationManager(manager, response); + + // update evolution icons + var isStarred = isAnnotationStarred(annotation); + onAnnotationCountChange(annotation.attr('data-date'), -1, isStarred ? -1 : 0); + } + ); + + return false; + }); + + // star/unstar annotation if star clicked + manager.on('click', '.annotation-star-changeable', function (e) { + var annotation = $(this).parent().parent(), + newStarredVal = $(this).attr('data-starred') == 0 ? 1 : 0 // flip existing 'starred' value + ; + + // perform ajax request to star annotation + annotationsApi.saveAnnotation( + idSite, + annotation.attr('data-id'), + annotation.attr('data-date'), + { + starred: newStarredVal + }, + function (response) { + replaceAnnotationHtml(annotation, response); + + // change starred count for this annotation in evolution graph based on what we're + // changing the starred value to + onAnnotationCountChange(annotation.attr('data-date'), 0, newStarredVal == 0 ? -1 : 1); + } + ); + }); + + // when period edit is clicked, show datepicker + manager.on('click', '.annotation-period-edit>a', function (e) { + e.preventDefault(); + $('.datepicker', $(this).parent()).toggle(); + return false; + }); + + // make sure datepicker popups are closed if someone clicks elsewhere + $('body').on('mouseup', function (e) { + var container = $('.annotation-period-edit>.datepicker:visible').parent(); + + if (!container.has(e.target).length) { + container.find('.datepicker').hide(); + } + }); + }; + +// used in below function + var loadingAnnotationManager = false; + + /** + * Shows an annotation manager under a report for a specific site & date range. + * + * @param {Element} domElem The element of the report to show the annotation manager + * under. + * @param {int} idSite The ID of the site to show the annotations of. + * @param {string} date The start date of the period. + * @param {string} period The period type. + * @param {int} lastN Whether to include the last N periods in the date range or not. Can + * be undefined. + * @param {function} [callback] + */ + var showAnnotationViewer = function (domElem, idSite, date, period, lastN, callback) { + var addToAnnotationCount = function (date, amt, starAmt) { + if (date.indexOf(',') != -1) { + date = date.split(',')[0]; + } + + $('.evolution-annotations>span[data-date]', domElem).each(function () { + if ($(this).attr('data-date') == date) { + // get counts from attributes (and convert them to ints) + var starredCount = +$(this).attr('data-starred'), + annotationCount = +$(this).attr('data-count'); + + // modify the starred count & make sure the correct image is used + var newStarCount = starredCount + starAmt; + var newAnno = 'icon-annotation'; + if (newStarCount > 0) { + newAnno += ' starred'; + } + $(this).attr('data-starred', newStarCount).find('span').attr('class', newAnno); + + // modify the annotation count & hide/show based on new count + var newCount = annotationCount + amt; + $(this).attr('data-count', newCount).css('opacity', newCount > 0 ? 1 : 0); + + return false; + } + }); + }; + + var manager = $('.annotation-manager', domElem); + if (manager.length) { + // if annotations for the requested date + period are already loaded, then just toggle the + // visibility of the annotation viewer. otherwise, we reload the annotations. + if (manager.attr('data-date') == date + && manager.attr('data-period') == period) { + // toggle manager view + if (manager.is(':hidden')) { + manager.slideDown('slow', function () { if (callback) callback(manager) }); + } + else { + manager.slideUp('slow', function () { if (callback) callback(manager) }); + } + } + else { + // show nothing but the loading gif + $('.annotations', manager).html(''); + $('.loadingPiwik', manager).show(); + + // reload annotation manager for new date/period + annotationsApi.getAnnotationManager(idSite, date, period, lastN, function (response) { + + replaceAnnotationManager(manager, response); + + createDatePickers(manager); + + // show if hidden + if (manager.is(':hidden')) { + manager.slideDown('slow', function () { if (callback) callback(manager) }); + } + else { + if (callback) { + callback(manager); + } + } + }); + } + } + else { + // if we are already loading the annotation manager, don't load it again + if (loadingAnnotationManager) { + return; + } + + loadingAnnotationManager = true; + + $('.loadingPiwikBelow', domElem).insertAfter($('.evolution-annotations', domElem)); + + var loading = $('.loadingPiwikBelow', domElem).css({display: 'block'}); + + // the annotations for this report have not been retrieved yet, so do an ajax request + // & show the result + annotationsApi.getAnnotationManager(idSite, date, period, lastN, function (response) { + var manager = $(response).hide(); + + // if an error occurred (and response does not contain the annotation manager), do nothing + if (!manager.hasClass('annotation-manager')) { + return; + } + + // create datepickers for each shown annotation + createDatePickers(manager); + + bindAnnotationManagerEvents(manager, idSite, addToAnnotationCount); + + loading.css('visibility', 'hidden'); + + placeAnnotationManager(manager, domElem); + + manager.slideDown('slow', function () { + loading.hide().css('visibility', 'visible'); + loadingAnnotationManager = false; + + if (callback) callback(manager) + }); + }); + } + }; + + /** + * Determines the x-coordinates of a set of evolution annotation icons. + * + * @param {Element} annotations The '.evolution-annotations' element. + * @param {Element} graphElem The evolution graph's datatable element. + */ + var placeEvolutionIcons = function (annotations, graphElem) { + var canvases = $('.piwik-graph .jqplot-xaxis canvas', graphElem), + noteSize = 16; + + // if no graph available, hide all icons + if (!canvases || canvases.length == 0) { + $('span[data-date]', annotations).hide(); + return true; + } + + // set position of each individual icon + $('span[data-date]', annotations).each(function (i) { + var canvas = $(canvases[i]), + canvasCenterX = canvas.position().left + (canvas.width() / 2); + $(this).css({ + left: canvasCenterX - noteSize / 2, + // show if there are annotations for this x-axis tick + opacity: +$(this).attr('data-count') > 0 ? 1 : 0 + }); + }); + }; + + var getPlotLinesLegendFooter = function (graphElem) { + var legendFooter = $('.jqplot-legend-footer', graphElem); + + if (legendFooter.length) { + return legendFooter; + } + + return $(); + }; + + var placeEvolutionAnnotations = function (annotations, graphElem) { + var legendFooter = getPlotLinesLegendFooter(graphElem); + + if (legendFooter.length) { + annotations.insertBefore(legendFooter); + return; + } + + annotations.insertBefore($('.dataTableFooterNavigation', graphElem)); + }; + + var placeAnnotationManager = function (manager, graphElem) { + var legendFooter = getPlotLinesLegendFooter(graphElem); + + if (legendFooter.length) { + manager.insertBefore(legendFooter); + return; + } + + manager.insertAfter($('.evolution-annotations', graphElem)); + }; + + // make annotation helpers globally accessible + piwik.annotations = { + showAnnotationViewer: showAnnotationViewer, + placeEvolutionIcons: placeEvolutionIcons, + placeEvolutionAnnotations: placeEvolutionAnnotations, + api: annotationsApi + }; + +}(jQuery, piwik)); diff --git a/plugins/Annotations/lang/am.json b/plugins/Annotations/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/am.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/ar.json b/plugins/Annotations/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..1deab052bcb73b11fa2dfa38e86bbbe71969a645 --- /dev/null +++ b/plugins/Annotations/lang/ar.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "أضف توضيحات عن %s…", + "AnnotationOnDate": "توضيحات عن %1$s: %2$s", + "Annotations": "التوضيحات", + "ClickToDelete": "انقر لحذف هذا التوضيح.", + "ClickToEdit": "انقر لتحرير هذا التوضيح.", + "ClickToEditOrAdd": "انقر لتحرير أو إضافة توضيح جديد.", + "ClickToStarOrUnstar": "انقر لتمييز أو أزالة تمييز هذا التوضيح.", + "CreateNewAnnotation": "إنشاء توضيح جديد…", + "EnterAnnotationText": "أكتب ملاحظتك…", + "HideAnnotationsFor": "إخفاء توضيحات %s…", + "IconDesc": "شاهد ملاحظات نطاق التاريخ هذا.", + "IconDescHideNotes": "إخفاء ملاحظات نطاق التاريخ هذا.", + "InlineQuickHelp": "يمكنك إنشاء توضيحات لإضافة أحداث مميزة (كمقال جديد بالمدونة، أو تصميم جديد للموقع)، لحفظ تحليل بياناتك أو لحفظ أي شيء آخر تراه مهماً.", + "LoginToAnnotate": "سجل الدخول لإنشاء توضيح.", + "NoAnnotations": "لا توجد توضيحات في هذا النطاق التاريخي.", + "PluginDescription": "يمكنك ارفاق ملاحظات لأيام مختلفة لتحديد تغييرات أجريتها على موقعك، حفظ تحليلك الذي أجريته فيما يتعلق بالبيانات ومشاركة أفكارك مع زملائك. بوضع توضيحات على بياناتك، ستتمكن من تذكر لماذا كانت بياناتك تبدو بهذه الطريقة.", + "ViewAndAddAnnotations": "مشاهدة وإضافة توضيحات حول %s…", + "YouCannotModifyThisNote": "لا يمكنك تحرير هذا التوضيح لأنك لم تنشأها، ولا تملك صلاحيات وصول مشرف في هذا الموقع." + } +} diff --git a/plugins/Annotations/lang/az.json b/plugins/Annotations/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/az.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/be.json b/plugins/Annotations/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/be.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/bg.json b/plugins/Annotations/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..9a1a82638a8634160eb422f7a9422b0370e6090e --- /dev/null +++ b/plugins/Annotations/lang/bg.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Добави анотации за %s…", + "AnnotationOnDate": "Анотация на %1$s: %2$s", + "Annotations": "Анотации", + "ClickToDelete": "Натиснете, за да изтриете този коментар.", + "ClickToEdit": "Цъкни за да редактираш тази анотация.", + "ClickToEditOrAdd": "Натисни за да редактираш или добавиш нова анотация.", + "ClickToStarOrUnstar": "Натиснете, за да отбележите или премахнете звезда за този коментар.", + "CreateNewAnnotation": "Създайте нова анотация…", + "EnterAnnotationText": "Въведете своята бележка…", + "HideAnnotationsFor": "Скриване на анотациите за %s…", + "IconDesc": "Вижте бележките за този период от време.", + "IconDescHideNotes": "Скриване на бележките за този период от време.", + "InlineQuickHelp": "Можете да създавате анотации, за да отбелязвате специални събития (като нова публикация в блог или нов изглед на сайт), за съхраняване на анализи или за запазване на нещо друго, което смятате за важно.", + "LoginToAnnotate": "Влезте в профила си за да създадете анотация.", + "NoAnnotations": "Няма анотации за този период от време.", + "PluginDescription": "Позволява да се прикрепят бележки към различни дни, за да бъдат отбелязани промените, направени във вашия сайт. Запазва анализите в зависимост от информацията, която е предоставена и споделя мнението ви с вашите колеги. Публикувайки данните ще имате възможност да запомните по какъв начин изглеждат те.", + "ViewAndAddAnnotations": "Преглеждане и добавяне на коментари за %s…", + "YouCannotModifyThisNote": "Вие не можете да променяте тази анотация, защото все още не сте я създали или нямате администраторски права за този сайт." + } +} diff --git a/plugins/Annotations/lang/bn.json b/plugins/Annotations/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/bn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/bs.json b/plugins/Annotations/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..0a4fb5d64e9e68ee3813adcbc18a2b6894ed78f4 --- /dev/null +++ b/plugins/Annotations/lang/bs.json @@ -0,0 +1,19 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Dodaj bilješku za %s…", + "AnnotationOnDate": "Bilješka za %1$s: %2$s", + "Annotations": "Bilješke", + "ClickToDelete": "Kliknite da obrišete ovu bilješku.", + "ClickToEdit": "Kliknite da uredite ovu bilješku.", + "ClickToEditOrAdd": "Kliknite da uredite ili dodate novu bilješku.", + "ClickToStarOrUnstar": "Kliknite da markirate bilješku ili da uklonite markiranje.", + "CreateNewAnnotation": "Napravi novu bilješku…", + "EnterAnnotationText": "Unesite vašu bilješku…", + "HideAnnotationsFor": "Sakrij bilješke za %s…", + "IconDesc": "Prikaži bilješke za ovaj vremenski period.", + "IconDescHideNotes": "Sakrij bilješke za ovaj vremenski period.", + "InlineQuickHelp": "Možete napraviti bilješke kako bi markirali posebne događaje (kao na primjer pravljenje novog članka na blogu, ili ponovno dizajniranje internet stranice), sačuvali vašu analizu podataka ili da sačuvate bilo šta drugo što smatrate važnim.", + "LoginToAnnotate": "Prijavite se da napravite novu bilješku.", + "NoAnnotations": "Nema bilješki za ovaj vremenski period." + } +} diff --git a/plugins/Annotations/lang/ca.json b/plugins/Annotations/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..deaaf2c815192f2cab94195beb5b1e57b6b7ad5d --- /dev/null +++ b/plugins/Annotations/lang/ca.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Afegir una anotació per %s…", + "AnnotationOnDate": "Anotació per %1$s: %2$s", + "Annotations": "Anotacions", + "ClickToDelete": "Feu click per eliminar l'anotació.", + "ClickToEdit": "Feu click per editar aquesta anotació.", + "ClickToEditOrAdd": "Feu click per editar o afegir una anotació.", + "ClickToStarOrUnstar": "Feu click per marcar o desmarcar aquesta nota.", + "CreateNewAnnotation": "Crea una nova anotació…", + "EnterAnnotationText": "Introduix la teva nota…", + "HideAnnotationsFor": "Amaga les anotacións per %s…", + "IconDesc": "Veure les notes d'aquest rang.", + "IconDescHideNotes": "Amaga les notes d'aquest rang.", + "InlineQuickHelp": "Podeu crear anotacions per marcar events especials (una nova entrada al blog, el rediseny del web). Les annotacions us permeten guardar el vostre anàlisis de la informació o qualsevol altra cosa que creïeu important.", + "LoginToAnnotate": "Identifiqueu-vos per crear una anotació.", + "NoAnnotations": "No hi ha notes per aquest rang de pàgines.", + "PluginDescription": "Permet afegir notes als diferents dies perquè pogueu recordar perquè la vostra inforamció es mostra d'una determinada forma.", + "ViewAndAddAnnotations": "Mostra i afegeix anotacions per %s…", + "YouCannotModifyThisNote": "No podeu modificar aquesta nota perquè o bé no l'heu creada vosatres, o no teni accès d'administrador per aquest lloc web." + } +} diff --git a/plugins/Annotations/lang/cs.json b/plugins/Annotations/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..dc3d3c36b783683f46c410638fd344267e0f292e --- /dev/null +++ b/plugins/Annotations/lang/cs.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Přidat anotace k %s…", + "AnnotationOnDate": "Anotace na %1$s: %2$s", + "Annotations": "Anotace", + "ClickToDelete": "Kliknutím odstraníte tuto anotaci.", + "ClickToEdit": "Klikněte k úpravě této anotace.", + "ClickToEditOrAdd": "Kliknutím upravte nebo přidejte novou anotaci.", + "ClickToStarOrUnstar": "Kliknutím ohnodnotíte tuto anotaci.", + "CreateNewAnnotation": "Vytvořit novou anotaci…", + "EnterAnnotationText": "Vložte poznámku…", + "HideAnnotationsFor": "Skrýt anotace %s…", + "IconDesc": "Zobrazit poznámky k tomuto časovému rozsahu.", + "IconDescHideNotes": "Skrýt poznámky zvoleného rozsahu.", + "InlineQuickHelp": "Můžete vytvořit anotace k důležitým událostem, jako je třeba nový příspěvek na blogu, redesign stránek, nebo cokoliv, co považujete za důležité.", + "LoginToAnnotate": "Přihlásit se pro vytvoření anotace.", + "NoAnnotations": "Nejsou zde žádné anotace k tomuto časovému rozsahu.", + "PluginDescription": "Umožňuje připojovat poznámky k různým dnům a označovat tak změny provedené na webu, ukládat provedené analýzy dat a sdílet své myšlenky s kolegy. Poznámkami k datům si zajistíte, že si budete pamatovat, proč vaše data vypadají tak, jak vypadají.", + "ViewAndAddAnnotations": "Zobrazit a přidat anotace k %s…", + "YouCannotModifyThisNote": "Nemůžete upravit tuto anotaci, protože jste ji nevytvořili a ani nemáte administrátorský přístup k této stránce." + } +} diff --git a/plugins/Annotations/lang/cy.json b/plugins/Annotations/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/da.json b/plugins/Annotations/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..c9d931eec4beb69dd43f794a8475fbe61dc30688 --- /dev/null +++ b/plugins/Annotations/lang/da.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Tilføj annotation for %s…", + "AnnotationOnDate": "Annotation den %1$s: %2$s", + "Annotations": "Anmærkninger", + "ClickToDelete": "Klik for at slette en annotation", + "ClickToEdit": "Klik for at rette en annotation", + "ClickToEditOrAdd": "Klik for at ændre eller tilføje en ny annotation.", + "ClickToStarOrUnstar": "Klik for at markere eller fjerne markering af anmærkningen.", + "CreateNewAnnotation": "Opret ny annotation…", + "EnterAnnotationText": "Indtast din note…", + "HideAnnotationsFor": "Skjul annotationer for %s…", + "IconDesc": "Vis noter for dette tidsrum.", + "IconDescHideNotes": "Skjul noter for dette tidsrum.", + "InlineQuickHelp": "Du kan oprette anmærkninger for at markere specielle begivenheder (som et nyt blog-indlæg, eller hjemmeside redesign), gemme dine data analyser eller at gemme noget andet, du synes er vigtigt.", + "LoginToAnnotate": "Log på for at oprette en anmærkning", + "NoAnnotations": "Der er ingen anmærkninger for datointervallet.", + "PluginDescription": "Giver mulighed for at knytte bemærkninger til forskellige dage, så du kan huske, hvorfor data ser ud som de gør.", + "ViewAndAddAnnotations": "Vis og tilføj annotationer for %s…", + "YouCannotModifyThisNote": "Du kan ikke ændre denne annotation, da du ikke har oprettet den eller har administrator adgang." + } +} diff --git a/plugins/Annotations/lang/de.json b/plugins/Annotations/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..212f67302dffe564ccd7e882541c8dc9d01191bc --- /dev/null +++ b/plugins/Annotations/lang/de.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Anmerkung für %s hinzufügen…", + "AnnotationOnDate": "Anmerkung für %1$s: %2$s", + "Annotations": "Anmerkungen", + "ClickToDelete": "Klicken Sie um diese Anmerkung zu löschen.", + "ClickToEdit": "Klicken Sie um diese Anmerkung zu bearbeiten.", + "ClickToEditOrAdd": "Klicken Sie hier um eine Anmerkung hinzuzufügen oder zu bearbeiten.", + "ClickToStarOrUnstar": "Klicken Sie, um die Markierung dieser Anmerkung zu setzen oder zu entfernen.", + "CreateNewAnnotation": "Neue Anmerkung anlegen…", + "EnterAnnotationText": "Geben Sie Ihre Notiz ein…", + "HideAnnotationsFor": "Anmerkungen für %s verbergen…", + "IconDesc": "Notizen für diesen Zeitraum anzeigen.", + "IconDescHideNotes": "Notizen für diesen Zeitraum verbergen.", + "InlineQuickHelp": "Sie können Anmerkungen erstellen um spezielle Ereignisse (wie einen Blog-Eintrag oder eine Neugestaltung der Webseite) zu markieren, Stichtage für die Datenanalyse zu speichern oder einfach alles zu speichern was Ihnen wichtig erscheint.", + "LoginToAnnotate": "Sie müssen sich einloggen um eine Anmerkung hinzufügen zu können.", + "NoAnnotations": "Für diesen Zeitraum sind keine Anmerkungen vorhanden.", + "PluginDescription": "Erlaubt Ihnen Anmerkungen zu verschiedenen Tagen hinzuzufügen, um Änderungen zu markieren, die an der Webseite gemacht wurden, Analysen zu speichern, die bezüglich Ihrer Daten gemacht wurden und um Ihre Gedanken mit Ihren Kollegen zu teilen. Durch das Hinzufügen von Anmerkungen stellen Sie sicher, dass Sie sich daran erinnern warum die Daten so sind, wie sie sind.", + "ViewAndAddAnnotations": "Anmerkungen zu %s ansehen und hinzufügen…", + "YouCannotModifyThisNote": "Sie können diese Anmerkung nicht ändern, da Sie sie weder erstellt haben noch administrativen Zugang für diese Seite besitzen." + } +} diff --git a/plugins/Annotations/lang/dv.json b/plugins/Annotations/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/el.json b/plugins/Annotations/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..fe2d004e6ca1ac1510df8abaafacc5e90cf7255b --- /dev/null +++ b/plugins/Annotations/lang/el.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Προσθήκη σχολίων για %s…", + "AnnotationOnDate": "Σχόλιο για %1$s: %2$s", + "Annotations": "Σχολιασμοί", + "ClickToDelete": "Πατήστε για να διαγράψετε αυτό το σχολιασμό.", + "ClickToEdit": "Πατήστε για να επεξεργαστείτε αυτό το σχολιασμό.", + "ClickToEditOrAdd": "Κάντε κλικ για να επεξεργαστείτε ή να προσθέσετε μια νέα σημείωση.", + "ClickToStarOrUnstar": "Πατήστε για να βάλετε ή να βγάλετε τη σημαία αυτού του σχολιασμού.", + "CreateNewAnnotation": "Δημιουργία νέου σχολιασμού…", + "EnterAnnotationText": "Εισάγετε τη σημείωσή σας…", + "HideAnnotationsFor": "Απόκρυψη σημειώσεων για %s…", + "IconDesc": "Προβολή σημειώσεων για αυτό το εύρος ημερομηνιών.", + "IconDescHideNotes": "Απόκρυψη σημειώσεων για αυτό το εύρος ημερομηνιών.", + "InlineQuickHelp": "Μπορείτε να δημιουργήσετε σημειώσεις για να επισημάνετε ειδικές εκδηλώσεις (όπως ένα νέο blog post, ή επανασχεδιασμό ιστοσελίδας), για να αποθηκεύσετε αναλύσεις των δεδομένων σας ή να αποθηκεύσετε ό,τι άλλο νομίζετε ότι είναι σημαντικό.", + "LoginToAnnotate": "Συνδεθείτε για να δημιουργήσετε ένα σχολιασμό.", + "NoAnnotations": "Δεν υπάρχουν σημειώσεις για αυτό το εύρος ημερομηνιών.", + "PluginDescription": "Σας επιτρέπει να επισυνάπτετε σημειώσεις σε διαφορετικές ημέρες για να σημειώνετε αλλαγές που συμβαίνουν στον ιστοτόπο σας, να κρατάτε τις αναλύσεις που κάνετε σχετικά με τα δεδομένα σας και να μοιράζεστε τις σκέψεις σας με τους συναδέλφους σας. Με την επισύναψη σημειώσεων, θα είστε σίγουροι ότι θα θυμάστε για ποιο λόγο τα δεδομένα σας φαίνονται έτσι.", + "ViewAndAddAnnotations": "Προβολή και προσθήκη σχολιασμών για %s…", + "YouCannotModifyThisNote": "Δεν μπορείτε να τροποποιήσετε αυτή τη σημείωση, γιατί δεν τη δημιουργήσατε, ούτε έχετε πρόσβαση διαχειριστή για αυτόν τον ιστοτόπο." + } +} diff --git a/plugins/Annotations/lang/en.json b/plugins/Annotations/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..3415625385da0bdb591d3086d2fd553c4d08259d --- /dev/null +++ b/plugins/Annotations/lang/en.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Add annotations for %s…", + "AnnotationOnDate": "Annotation on %1$s: %2$s", + "Annotations": "Annotations", + "ClickToDelete": "Click to delete this annotation.", + "ClickToEdit": "Click to edit this annotation.", + "ClickToEditOrAdd": "Click to edit or add a new annotation.", + "ClickToStarOrUnstar": "Click to star or unstar this annotation.", + "CreateNewAnnotation": "Create a new annotation…", + "EnterAnnotationText": "Enter your note…", + "HideAnnotationsFor": "Hide annotations for %s…", + "IconDesc": "View notes for this date range.", + "IconDescHideNotes": "Hide notes for this date range.", + "InlineQuickHelp": "You can create annotations to mark special events (like a new blog post, or website redesign), to save your data analyses or to save anything else you think is important.", + "LoginToAnnotate": "Login to create an annotation.", + "NoAnnotations": "There are no annotations for this date range.", + "PluginDescription": "Allows you to attach notes to different days to mark changes made to your website, save analyses you make regarding your data and share your thoughts with your colleagues. By annotating your data, you will make sure you remember why your data looks the way it does.", + "ViewAndAddAnnotations": "View and add annotations for %s…", + "YouCannotModifyThisNote": "You cannot modify this annotation, because you did not create it, nor do you have admin access for this site." + } +} \ No newline at end of file diff --git a/plugins/Annotations/lang/eo.json b/plugins/Annotations/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/eo.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/es-ar.json b/plugins/Annotations/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..3f603fe1d39afad67fd0fa90d25cc336fd7ff2b6 --- /dev/null +++ b/plugins/Annotations/lang/es-ar.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Agregar anotaciones para %s…", + "AnnotationOnDate": "Anotaciones en %1$s: %2$s", + "Annotations": "Anotaciones", + "ClickToDelete": "Hacé clic para eliminar esta anotación.", + "ClickToEdit": "Hacé clic para editar esta anotación.", + "ClickToEditOrAdd": "Hacé clic para editar o agregar una nueva anotación.", + "ClickToStarOrUnstar": "Hacé clic para destacar o quitar destacado a esta anotación.", + "CreateNewAnnotation": "Crear una nueva anotación…", + "EnterAnnotationText": "Ingresá tu nota…", + "HideAnnotationsFor": "Ocultar anotaciones para %s…", + "IconDesc": "Ver las notas en este rango de fechas.", + "IconDescHideNotes": "Ocultar las notas en este rango de fechas.", + "InlineQuickHelp": "Podés crear anotaciones para marcar eventos especiales (como un nuevo artículo en el blog o el rediseño del sitio), para guardar tus análisis de los datos o para guardar cualquier otra cosa que te parezca importante.", + "LoginToAnnotate": "Iniciá sesión para crear una anotación.", + "NoAnnotations": "No existen anotaciones en este rango de fechas.", + "PluginDescription": "Te permite adjuntar notas en diferentes¿s días para marcar cambios hechos a tu sitio web, guardar los análisis que hayás hecho sobre los datos y compartir tus ideas con tus colegas. Al agregar anotaciones a tus datos te asegurarás de recordar por qué los datos lucen así.", + "ViewAndAddAnnotations": "Ver y agregar anotaciones para %s…", + "YouCannotModifyThisNote": "No podés modificar esta anotación porque ni la creaste vos, ni tenés privilegios de administrador en este sitio." + } +} diff --git a/plugins/Annotations/lang/es.json b/plugins/Annotations/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..7e64d821a92ef4966f4e1a28761608592068747c --- /dev/null +++ b/plugins/Annotations/lang/es.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Añadir anotaciones para %s…", + "AnnotationOnDate": "Anotaciones en %1$s: %2$s", + "Annotations": "Anotaciones", + "ClickToDelete": "Haga clic para eliminar esta anotación.", + "ClickToEdit": "Haz clic para editar esta anotación.", + "ClickToEditOrAdd": "Clic para editar o añadir una nueva anotación.", + "ClickToStarOrUnstar": "Haz clic para destacar o quitar destacado a esta anotación.", + "CreateNewAnnotation": "Crear una nueva anotación…", + "EnterAnnotationText": "Escriba su nota…", + "HideAnnotationsFor": "Ocultar anotaciones para %s…", + "IconDesc": "Ver las notas en este rango de fechas.", + "IconDescHideNotes": "Ocultar las notas en este rango de fechas.", + "InlineQuickHelp": "Puede crear anotaciones para señalar eventos especiales (como un nuevo artículo en el blog o el rediseño del sitio de internet), para guardar sus análisis de los datos o para guardar cualquier otra cosa que considere importante.", + "LoginToAnnotate": "Inicia sesión para crear una anotación.", + "NoAnnotations": "No existen anotaciones en este rango de fechas.", + "PluginDescription": "Le permite adjuntar notas en diferentes días para señalar cambios que haya realizado en su sitio de internet, guardar los análisis que haya realizado sobre los datos y compartir sus ideas con sus colegas. Al agregar anotaciones a sus datos se asegurará recordar el por qué de sus datos se vean de ese modo.", + "ViewAndAddAnnotations": "Ver y añadir anotaciones para %s…", + "YouCannotModifyThisNote": "No puede modificar esta anotación debido a que ni la creó ni posee privilegios de administrador en este sitio." + } +} diff --git a/plugins/Annotations/lang/et.json b/plugins/Annotations/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..fdc704452f891d90b64f38ccf36ad8bafc56ae47 --- /dev/null +++ b/plugins/Annotations/lang/et.json @@ -0,0 +1,19 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Lisa annotatsioon %s'le…", + "AnnotationOnDate": "%1$s annotatsioon: %2$s", + "Annotations": "Annotatsioone", + "ClickToDelete": "Vajuta antud annotatsiooni kustutamiseks.", + "ClickToEdit": "Vajuta antud annotatsiooni redigeerimiseks.", + "ClickToEditOrAdd": "Vajuta antud annotatsiooni redigeerimiseks või lisamiseks.", + "ClickToStarOrUnstar": "Vali et kommentaari hinnata", + "CreateNewAnnotation": "Loo uus kommentaar…", + "EnterAnnotationText": "Sisesta oma märkmed…", + "HideAnnotationsFor": "Peida %s kommentaarid…", + "IconDesc": "Vaata antud ajavahemiku märkmeid.", + "IconDescHideNotes": "Peida antud ajavahemiku märkmed.", + "LoginToAnnotate": "Sisene, et luua kommentaar.", + "NoAnnotations": "Antud ajavahemiku kohta puuduvad märkmed.", + "ViewAndAddAnnotations": "Vaata ja lisa märkmeid %s kohta…" + } +} diff --git a/plugins/Annotations/lang/eu.json b/plugins/Annotations/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..df24a4988011abcb60eed91351181548e690baca --- /dev/null +++ b/plugins/Annotations/lang/eu.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Gehitu oharrak %s(r)entzat…", + "AnnotationOnDate": "%1$s(r)i buruzko oharra: %2$s", + "Annotations": "Oharrak", + "ClickToDelete": "Egin klik ohar hau ezabatzeko.", + "ClickToEdit": "Egin klik ohar hau editatzeko.", + "ClickToEditOrAdd": "Egin klik ohar berria gehitu edo editatzeko.", + "ClickToStarOrUnstar": "Egin klik ohar hau gogoko egin edo gogokoa kentzeko.", + "CreateNewAnnotation": "Sortu ohar berria…", + "EnterAnnotationText": "Idatzi oharra…", + "HideAnnotationsFor": "Ezkutatu %s(r)en oharrak…", + "IconDesc": "Ikusi data-tarte honetako oharrak.", + "IconDescHideNotes": "Ezkutatu data-tarte honetako oharrak.", + "InlineQuickHelp": "Oharrak sortu ditzakezu gertaera bereziak markatzeko (blog bidalketa berria edo webgunearen diseinua berritzea), datu-analisiak gordetzeko edo garrantzitsua iruditzen zaizun edozein gauza gordetzeko.", + "LoginToAnnotate": "Hasi saioa oharra sortzeko.", + "NoAnnotations": "Ez dago oharrik data-tarte honetarako.", + "PluginDescription": "Aukera ematen dizu webgunean aldaketak zein egunetan egin diren ohar bidez adierazteko, zure datuei buruzko analisiak gordetzeko eta zure lankideekin ideiak partekatzeko. Zure datuei oharrak gehituz ziurtatu dezakezu gogoratuko duzula zergatik diren zure datuak diruditen bezalakoak.", + "ViewAndAddAnnotations": "Ikusi eta gehitu oharrak %s(r)entzat…", + "YouCannotModifyThisNote": "Ezin duzu ohar hau aldatu, ez duzulako zuk sortu eta ez duzulako gune honetara administratzaile sarbiderik ere." + } +} diff --git a/plugins/Annotations/lang/fa.json b/plugins/Annotations/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..68437e5af4d2d0e224ad574e1db9443155296b48 --- /dev/null +++ b/plugins/Annotations/lang/fa.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "توضیحاتی اضافه کنید برای %s…", + "AnnotationOnDate": "توضیحات از %1$s:%2$s", + "Annotations": "توضیحات", + "ClickToDelete": "برای پاک کردن توضیح کلیک کنید.", + "ClickToEdit": "برای ویرایش توضیح کلیک کنید.", + "ClickToEditOrAdd": "برای ویرایش یا اضافه کردن یک توضیح جدید کلیک کنید.", + "ClickToStarOrUnstar": "برای ویژه کردن/نکردن توضیح کلیک کنید.", + "CreateNewAnnotation": "یک توضیح جدید بسازید…", + "EnterAnnotationText": "یادداشت تان را وارد کنید…", + "HideAnnotationsFor": "توضیحاتی پنهان کنید برای %s…", + "IconDesc": "یادداشت های این بازه زمانی را مشاهده کنید.", + "IconDescHideNotes": "یادداشت های این بازه زمانی را پنهان کنید.", + "InlineQuickHelp": "شما می توانید توضیحاتی را برای علامت گذاری رخداد ویژه ای (مثلا یک مطلب جدید وبلاگ یا طراحی دوباره وبسایت)ایجاد کنید.این برای نگهداری تحلیل داده هایتان یا هر چیزی دیگری است که شما فکر می کنید مهم است.", + "LoginToAnnotate": "برای ایجاد توضیح وارد سیستم شوید.", + "NoAnnotations": "برای بازه زمانی انتخابی یادداشتی موجود نیست.", + "PluginDescription": "به شما امکان می دهد یادداشت‌هایی را به روزهای مختلف پیوست کنید تا تغییرات ایجاد شده در وب سایت خود را علامت گذاری کنید، تجزیه و تحلیل هایی که در مورد داده‌های خود انجام می دهید ذخیره کنید و اندیشه‌های خود را با همکارانتان به اشتراک بگذارید. با حاشیه نویسی داده های خود، مطمئن خواهید شد که به خاطر می آورید چرا داده های شما به این شکل به نظر می رسد.", + "ViewAndAddAnnotations": "نمایش و افزودن توضیحات برای %s…", + "YouCannotModifyThisNote": "شما نمی توانید این توضیح را تغییر دهید , زیرا شما آن را ایجاد نکرده اید و نه اجازه دسترسی مدیر برای این سایت را دارید." + } +} diff --git a/plugins/Annotations/lang/fi.json b/plugins/Annotations/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..f9a55dccdd465b656b5cc268284f307af134448f --- /dev/null +++ b/plugins/Annotations/lang/fi.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Lisää kommentti %s:lle…", + "AnnotationOnDate": "Kommentti %1$s:lle: %2$s", + "Annotations": "Kommentit", + "ClickToDelete": "Klikkaa poistaaksesi tämä kommentti.", + "ClickToEdit": "Klikkaa muokataksesi tätä kommenttia.", + "ClickToEditOrAdd": "Klikkaa muokataksesi kommenttia tai lisätäksesi uuden kommentin.", + "ClickToStarOrUnstar": "Klikkaa merkitäksesi tähdellä tai poistaaksesi tähtimerkinnän tästä kommentista.", + "CreateNewAnnotation": "Luo uusi kommentti…", + "EnterAnnotationText": "Kirjoita kommenttisi…", + "HideAnnotationsFor": "Piilota %s:n kommentit…", + "IconDesc": "Näytä kommentit tältä aikaväliltä.", + "IconDescHideNotes": "Piilota kommentit tältä aikaväliltä.", + "InlineQuickHelp": "Voit luoda kommentteja erityisiä tapahtumia varten (kuten uusi blogiteksti tai verkkosivun uusi design), tallentaaksesi data-analyysejä tai muuta tärkeäksi katsomaasi.", + "LoginToAnnotate": "Kirjaudu sisään luodaksesi kommentti.", + "NoAnnotations": "Tällä aikavälillä ei ole kommentteja.", + "PluginDescription": "Mahdollistaa kommenttien lisäämisen eri päiville, jotta voit merkitä verkkosivulle tekemiäsi muutoksia, tallentaa analyysejä ja jakaa ajatuksia kollegoidesi kanssa. Kommenttien avulla muistat, miksi data näyttää siltä kuin näyttää.", + "ViewAndAddAnnotations": "Tarkastele ja lisää kommentteja %s:lle…", + "YouCannotModifyThisNote": "Et voi muokata tätä kommenttia, koska et ole luonut sitä, eikä sinulla ole admin-oikeuksia tällä verkkosivulla." + } +} diff --git a/plugins/Annotations/lang/fr.json b/plugins/Annotations/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..f71bde928c6be915de82452a17ec0ab7912805d2 --- /dev/null +++ b/plugins/Annotations/lang/fr.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Ajouter des annotations pour %s…", + "AnnotationOnDate": "Annotation sur %1$s : %2$s", + "Annotations": "Annotations", + "ClickToDelete": "Cliquez pour supprimer cette annotation.", + "ClickToEdit": "Cliquez pour modifier cette annotation.", + "ClickToEditOrAdd": "Cliquez pour modifier ou ajouter une nouvelle annotation.", + "ClickToStarOrUnstar": "Cliquez pour mettre en favori cette annotation ou l'enlever.", + "CreateNewAnnotation": "Créer une nouvelle annotation…", + "EnterAnnotationText": "Entrez votre note…", + "HideAnnotationsFor": "Masquer les annotations pour %s…", + "IconDesc": "Afficher les notes pour cette plage de dates.", + "IconDescHideNotes": "Masquer les notes pour cette plage de dates.", + "InlineQuickHelp": "Vous pouvez créer des annotations pour marquer des événements spéciaux (comme un nouvel article de blog ou une refonte de site Web), pour enregistrer vos analyses de données ou pour enregistrer tout autre élément que vous jugez important.", + "LoginToAnnotate": "Connectez-vous pour créer une annotation.", + "NoAnnotations": "Il n'y a pas d'annotations pour cette plage de dates.", + "PluginDescription": "Vous permet de joindre des notes à différents jours pour marquer les modifications apportées à votre site Web, enregistrer les analyses que vous faites concernant vos données et partager vos réflexions avec vos collègues. En annotant vos données, vous vous assurerez de vous rappeler pourquoi vos données ressemblent à ce qu'elles sont.", + "ViewAndAddAnnotations": "Afficher et ajouter des annotations pour %s…", + "YouCannotModifyThisNote": "Vous ne pouvez pas modifier cette annotation, car vous ne l'avez pas créée et vous n'avez pas non plus d'accès administrateur pour ce site." + } +} diff --git a/plugins/Annotations/lang/ga.json b/plugins/Annotations/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..397dea052f1d8259a8499f63ab07d61de94168e5 --- /dev/null +++ b/plugins/Annotations/lang/ga.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Cuir nótaí le %s leis…", + "AnnotationOnDate": "Anótáil ar %1$s: %2$s", + "Annotations": "Anótálacha", + "ClickToDelete": "Cliceáil chun an nóta seo a scriosadh.", + "ClickToEdit": "Cliceáil chun an nóta seo a chur in eagar.", + "ClickToEditOrAdd": "Cliceáil chun nóta nua a chur in eagar nó a chur leis.", + "ClickToStarOrUnstar": "Cliceáil chun an nóta seo a réiltín nó a bhaint de.", + "CreateNewAnnotation": "Cruthaigh nóta nua…", + "EnterAnnotationText": "Cuir isteach do nóta…", + "HideAnnotationsFor": "Folaigh nótaí do %s…", + "IconDesc": "Féach ar nótaí don raon dátaí seo.", + "IconDescHideNotes": "Folaigh nótaí don raon dáta seo.", + "InlineQuickHelp": "Is féidir leat nótaí a chruthú chun imeachtaí speisialta a mharcáil (cosúil le blagphost nua, nó athdhearadh láithreán gréasáin), chun do anailísí sonraí a shábháil nó chun aon rud eile a cheapann tú atá tábhachtach a shábháil.", + "LoginToAnnotate": "Logáil isteach chun nóta a chruthú.", + "NoAnnotations": "Níl aon nótaí ann don raon dátaí seo.", + "PluginDescription": "Ligeann sé duit nótaí a cheangal le laethanta éagsúla chun athruithe a rinneadh ar do shuíomh Gréasáin a mharcáil, chun anailísí a dhéanann tú ar do shonraí a shábháil agus chun do chuid smaointe a roinnt le do chomhghleacaithe. Trí do shonraí a anótáil, cinnteoidh tú go gcuimhneoidh tú cén fáth a bhreathnaíonn do shonraí mar a dhéanann siad.", + "ViewAndAddAnnotations": "Féach ar agus cuir nótaí le %s leis…", + "YouCannotModifyThisNote": "Ní féidir leat an nóta seo a mhodhnú, toisc nár chruthaigh tú é, agus níl rochtain riaracháin agat ar an suíomh seo." + } +} diff --git a/plugins/Annotations/lang/gl.json b/plugins/Annotations/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..4a366d6c9133bb992593163382e67c8912a91ff9 --- /dev/null +++ b/plugins/Annotations/lang/gl.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Engadir anotación para %s…", + "AnnotationOnDate": "Anotación sobre %1$s: %2$s", + "Annotations": "Anotacións", + "ClickToDelete": "Prema para eliminar esta anotación.", + "ClickToEdit": "Prema para editar esta anotación.", + "ClickToEditOrAdd": "Prema para editar ou engadir unha anotación nova.", + "ClickToStarOrUnstar": "Haga clic para marcar o desmarcar esta anotación.", + "CreateNewAnnotation": "Crear unha anotación nova…", + "EnterAnnotationText": "Introduza a súa nota…", + "HideAnnotationsFor": "Ocultar anotaciones para %s…", + "IconDesc": "Ver notas para este intervalo de fechas.", + "IconDescHideNotes": "Ocultar notas para este intervalo de fechas.", + "InlineQuickHelp": "Puede crear anotaciones para marcar eventos especiales (como una nueva publicación de blog o el rediseño de un sitio web), para guardar sus análisis de datos o para guardar cualquier otra cosa que considere importante.", + "LoginToAnnotate": "Inicie sesión para crear una anotación.", + "NoAnnotations": "No hay anotaciones para este intervalo de fechas.", + "PluginDescription": "Le permite adjuntar notas a diferentes días para marcar los cambios realizados en su sitio web, guardar los análisis que realiza con respecto a sus datos y compartir sus pensamientos con sus colegas. Al anotar sus datos, se asegurará de recordar por qué sus datos tienen el aspecto que tienen.", + "ViewAndAddAnnotations": "Ver y agregar anotaciones para %s…", + "YouCannotModifyThisNote": "No puede modificar esta anotación porque no la creó usted ni tiene acceso de administrador para este sitio." + } +} diff --git a/plugins/Annotations/lang/gu.json b/plugins/Annotations/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..80530900f6e87109be78d41d8a4ec5709988acff --- /dev/null +++ b/plugins/Annotations/lang/gu.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "%s માટે ટીકાઓ ઉમેરો…", + "AnnotationOnDate": "%1$s પર ટીકા: %2$s", + "Annotations": "ટીકાઓ", + "ClickToDelete": "આ ટીકાને કાઢી નાખવા ક્લિક કરો.", + "ClickToEdit": "આ ટીકાને સંપાદિત કરવા ક્લિક કરો.", + "ClickToEditOrAdd": "નવી ટીકાને સંપાદિત કરવા અથવા ઉમેરવા માટે ક્લિક કરો.", + "ClickToStarOrUnstar": "આ ટીકાને તારાંકિત કરવા અથવા અતારાંકિત કરવા માટે ક્લિક કરો.", + "CreateNewAnnotation": "નવી ટીકા બનાવો…", + "EnterAnnotationText": "તમારી નોંધ દાખલ કરો…", + "HideAnnotationsFor": "%s માટે ટીકાઓ છુપાવો…", + "IconDesc": "આ તારીખ શ્રેણી માટે નોંધો જુઓ.", + "IconDescHideNotes": "આ તારીખ શ્રેણી માટે નોંધો છુપાવો.", + "InlineQuickHelp": "તમે વિશિષ્ટ ઇવેન્ટ્સને ચિહ્નિત કરવા માટે ટીકાઓ બનાવી શકો છો (જેમ કે નવી બ્લોગ પોસ્ટ, અથવા વેબસાઇટ રીડિઝાઇન), તમારા ડેટા વિશ્લેષણને સાચવવા માટે અથવા તમને મહત્વપૂર્ણ લાગે છે તે કંઈપણ સાચવવા માટે.", + "LoginToAnnotate": "એક ટીકા બનાવવા માટે લોગિન કરો.", + "NoAnnotations": "આ તારીખ શ્રેણી માટે કોઈ ટીકાઓ નથી.", + "PluginDescription": "તમારી વેબસાઇટ પર થયેલા ફેરફારોને ચિહ્નિત કરવા, તમારા ડેટાને લગતા તમે કરેલા વિશ્લેષણોને સાચવવા અને તમારા સાથીદારો સાથે તમારા વિચારો શેર કરવા માટે તમને જુદા જુદા દિવસોની નોંધો જોડવાની મંજૂરી આપે છે. તમારા ડેટાની ટીકા કરીને, તમે ખાતરી કરશો કે તમને યાદ છે કે તમારો ડેટા શા માટે તે જેવો દેખાય છે.", + "ViewAndAddAnnotations": "%s માટે ટીકાઓ જુઓ અને ઉમેરો…", + "YouCannotModifyThisNote": "તમે આ ટીકાને સંશોધિત કરી શકતા નથી, કારણ કે તમે તેને બનાવ્યું નથી, કે તમારી પાસે આ સાઇટ માટે એડમિન એક્સેસ નથી." + } +} diff --git a/plugins/Annotations/lang/he.json b/plugins/Annotations/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..45a1e4c9f276a04a125e8454d910c5cdb7636918 --- /dev/null +++ b/plugins/Annotations/lang/he.json @@ -0,0 +1,15 @@ +{ + "Annotations": { + "Annotations": "הערות הסבר", + "ClickToDelete": "לחץ כדי למחוק הערה זו.", + "ClickToEdit": "לחץ כדי לערוך הערה זו.", + "ClickToEditOrAdd": "לחץ כדי לערוך או להוסיף הערה חדשה.", + "ClickToStarOrUnstar": "לחץ כדי לסמן או להוריד סימון מהערה זו.", + "CreateNewAnnotation": "יצירת הערה חדשה…", + "EnterAnnotationText": "הקלידו הערה…", + "IconDesc": "הראה הערות לטווח תאריכים המבוקש", + "IconDescHideNotes": "הסתר הערות של טווח תאריכים זה", + "LoginToAnnotate": "יש להתחבר כדי ליצור הערה.", + "NoAnnotations": "אין הסברים על טווח תאריכים זה" + } +} diff --git a/plugins/Annotations/lang/hi.json b/plugins/Annotations/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..76d74058f682b883740847caa7fe071d04a2aace --- /dev/null +++ b/plugins/Annotations/lang/hi.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "%s एनोटेशन जोड़ें…", + "AnnotationOnDate": "%1$s एनोटेशन तारीख: %2$s", + "Annotations": "टिप्पणियाँ", + "ClickToDelete": "इस टिपण्णी को हटाने के लिए क्लिक करें।", + "ClickToEdit": "इस टिपण्णी में बदलाव करने के लिए क्लिक करें।", + "ClickToEditOrAdd": "टिपण्णी में बदलाव करने के लिए या नयी टिपण्णी बनाने के लिए क्लिक करें।", + "ClickToStarOrUnstar": "इस टिप्पणी को स्टार या अतारांकित करने के लिए क्लिक करें.", + "CreateNewAnnotation": "नयी टिपण्णी बनाएं।…", + "EnterAnnotationText": "अपनी टिप्पणी दर्ज करें …", + "HideAnnotationsFor": "%s एनोटेशन छुपाएं…", + "IconDesc": "इस समय अवधि के लिए नोट्स देखें.", + "IconDescHideNotes": "इस समय अवधि के लिए नोट्स छुपाये .", + "InlineQuickHelp": "आप विशेष घटनाओं (जैसे की नया ब्लॉग पोस्ट या वेबसाइट को पुनः डिज़ाइन करना) को अंकित करने के लिए या अपने डेटा के विश्लेषण अथवा ऐसी कोई भी चीज़ जो कि आप के अनुसार महत्वपूर्ण है को सुरक्षित रखने के लिए टिप्पणियों का प्रयोग कर सकते हैं।", + "LoginToAnnotate": "नयी टिपण्णी बनाने के लिए लौग इन करें।", + "NoAnnotations": "स्पष्ट की गयी तारीख की सीमा के लिए कोई टिपण्णी मौजूद नहीं है।", + "PluginDescription": "आप अपनी वेबसाइट पर किए गए परिवर्तनों को चिह्नित करने के लिए अलग अलग दिनों के लिए नोट संलग्न करने के लिए अनुमति देता है,विश्लेषण बचाने आप अपने डेटा को को बनाने और अपने सहयोगियों के साथ अपने विचारों को साझा करें. अपने डेटा एनोटेट करके, आप सुनिश्चित करे क्यों अपने डेटा ऐसा लग रहा है", + "ViewAndAddAnnotations": "%s टिप्पणियां देखें और जोड़…", + "YouCannotModifyThisNote": "आप इस टिपण्णी में परिवर्तन नहीं कर सकते हैं क्योंकि न तो आपने इसके लेखक हैं और न ही आपके पास इस साइट के लिए एडमिन एक्सेस है।" + } +} diff --git a/plugins/Annotations/lang/hr.json b/plugins/Annotations/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..bb818873dba0cdfdc0a9c2ffa4927c537330a433 --- /dev/null +++ b/plugins/Annotations/lang/hr.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Dodaj bilješke za %s…", + "AnnotationOnDate": "Bilješke za %1$s: %2$s", + "Annotations": "Bilješke", + "ClickToDelete": "Kliknite izbrisati ovu obavijest.", + "ClickToEdit": "Klikni za urediti ovu obavijest.", + "ClickToEditOrAdd": "Klikni za uređivanje ili dodavanje nove bilješke.", + "ClickToStarOrUnstar": "Klikni za označavanje ili odznačavanje ove bilješke.", + "CreateNewAnnotation": "Stvori novu bilješku …", + "EnterAnnotationText": "Upiši svoju bilješku …", + "HideAnnotationsFor": "Sakrij bilješke za %s …", + "IconDesc": "Pobledaj bilješke za ovaj raspon datuma.", + "IconDescHideNotes": "Sakrij bilješke za ovaj raspon datuma.", + "InlineQuickHelp": "Možeš stvoriti bilješke za označavanje posebnih događaja (poput nove objave na blogu ili redizajn web stranice), za spremanje analiza podataka ili za spremanje bilo čega drugog što smatraš važnim.", + "LoginToAnnotate": "Prijavi se za objaviti obavijest.", + "NoAnnotations": "Nema bilješki za ovaj raspon datuma.", + "PluginDescription": "Omogućuje dodavanje bilješki različitim danima za označavanje promjena na tvojoj web stranici, čuvanje analize u vezi tvojih podataka i dijelila tvoja razmišljanja s kolegama. Dodavanjem biljžaka tojim podataka osiguravaš da se sjećaš zašto tvoji podaci izgledaju kako izgledaju.", + "ViewAndAddAnnotations": "Pogledaj i dodaj bilješke za %s …", + "YouCannotModifyThisNote": "Ovu bilješku ne možeš mijenjati jer je nisi stvorio/la, niti imaš administratorski pristup za ovu stranicu." + } +} diff --git a/plugins/Annotations/lang/hu.json b/plugins/Annotations/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/hu.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/hy.json b/plugins/Annotations/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/id.json b/plugins/Annotations/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..92b9de2d54dc2fe418aa7861c7c22da412bd9623 --- /dev/null +++ b/plugins/Annotations/lang/id.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Tambahkan anotasi untuk %s…", + "AnnotationOnDate": "Anotasi pada %1$s: %2$s", + "Annotations": "Anotasi", + "ClickToDelete": "Klik untuk menghapus anotasi ini.", + "ClickToEdit": "Klik untuk mengedit anotasi ini.", + "ClickToEditOrAdd": "Klik untuk mengedit atau menambahkan anotasi baru.", + "ClickToStarOrUnstar": "Klik untuk memberi atau menghapus bintang pada anotasi ini.", + "CreateNewAnnotation": "Buat anotasi baru…", + "EnterAnnotationText": "Masukkan catatan Anda…", + "HideAnnotationsFor": "Sembunyikan anotasi untuk %s…", + "IconDesc": "Lihat catatan untuk rentang data ini.", + "IconDescHideNotes": "Sembunyikan catatan untuk rentang tanggal ini.", + "InlineQuickHelp": "Anda dapat membuat anotasi untuk menandai peristiwa istimewa (seperti sebuah kiriman blog baru atau perubahan desain situs), untuk menyimpan analisa data Anda atau untuk menyimpan hal lain yang menurut Anda penting.", + "LoginToAnnotate": "Log masuk untuk membuat anotasi.", + "NoAnnotations": "Tidak tersedia anotasi untuk rentang tanggal ini.", + "PluginDescription": "Memungkinkan Anda untuk melampirkan catatan dari hari berbeda untuk menandai perubahan dalam situs Anda, simpan analisa mengenai data Anda dan berbagi pemikiran tersebut dengan rekan Anda. Dengan memberi penjelasan terhadap data, akan membuat Anda ingat mengapa data tampak seperti saat ini terjadi.", + "ViewAndAddAnnotations": "Tampilkan dan tambahkan anotasi untuk %s…", + "YouCannotModifyThisNote": "Anda tidak dapat mengubah anotasi ini, sebab Anda bukanlah pembuatnya, atau Anda tidak memiliki akses admin untuk situs ini." + } +} diff --git a/plugins/Annotations/lang/is.json b/plugins/Annotations/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/is.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/it.json b/plugins/Annotations/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..6c50df0467e3394bab3465d14f9e921acc23c361 --- /dev/null +++ b/plugins/Annotations/lang/it.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Aggiungi annotazioni per %s…", + "AnnotationOnDate": "Annotazione su %1$s: %2$s", + "Annotations": "Annotazioni", + "ClickToDelete": "Cancella questa annotazione.", + "ClickToEdit": "Modifica questa annotazione.", + "ClickToEditOrAdd": "Modifica o aggiungi una nuova annotazione.", + "ClickToStarOrUnstar": "Segna questa annotazione.", + "CreateNewAnnotation": "Crea una nuova annotazione…", + "EnterAnnotationText": "Inserisci annotazione…", + "HideAnnotationsFor": "Nascondi annotazioni per %s…", + "IconDesc": "Visualizza le annotazioni per questo intervallo di date.", + "IconDescHideNotes": "Nascondi le annotazioni in questo intervallo di date.", + "InlineQuickHelp": "Potete creare delle note per segnale eventi speciali (quali un nuovo post in un blog o il restyling di un sito), per salvare i vostri dati di analisi o per salvare qualcos'altro voi pensiate sia importante.", + "LoginToAnnotate": "Accedi per creare un'annotazione.", + "NoAnnotations": "Non ci sono annotazioni per questo intervallo di date.", + "PluginDescription": "Vi permette di associare delle note a vari giorni per segnare i cambiamenti fatti al vostro sito, salvare le analisi da voi fatte sui vostri dati e condividere i vostri pensieri con i colleghi.", + "ViewAndAddAnnotations": "Visualizza ed aggiungi annotazioni per %s…", + "YouCannotModifyThisNote": "Non potete modificare questa annotazione perché non l'avete creata voi, e non avete i privilegi di amministratore per questo sito." + } +} diff --git a/plugins/Annotations/lang/ja.json b/plugins/Annotations/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..46633e9edbe89f0db03d48210ce2a15c79bf53b8 --- /dev/null +++ b/plugins/Annotations/lang/ja.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "%sのアノテーションを追加…", + "AnnotationOnDate": "%1$sのアノテーション: %2$s", + "Annotations": "アノテーション", + "ClickToDelete": "クリックしてこの注釈を削除します。", + "ClickToEdit": "クリックしてこの注釈を編集します。", + "ClickToEditOrAdd": "クリックして、新しい注釈を編集または追加します。", + "ClickToStarOrUnstar": "クリックして、この注釈にスターを付けたり外したりします。", + "CreateNewAnnotation": "新しい注釈を作成します…", + "EnterAnnotationText": "メモを入力して下さい…", + "HideAnnotationsFor": "%sのアノテーションを非表示にする…", + "IconDesc": "この期間のメモを表示します。", + "IconDescHideNotes": "この期間のメモを非表示にします。", + "InlineQuickHelp": "データの分析結果や重要と思うものを記録するために、特別なイベントとしてアノテーションを作成できます ( 新しいブログの投稿、ウェブサイトのリニューアルなど ) 。", + "LoginToAnnotate": "アノテーションを作成するにはログインしてください。", + "NoAnnotations": "この日付範囲の注釈はありません。", + "PluginDescription": "ウェブサイトに加えられた変更をマークしたり、データに関して行った分析を保存したり、あなたの考えを同僚と共有したりするために、それぞれの日にノートを添付することができます。 データにアノテーションを付けることで、そのデータの根拠を思い出すきっかけになります。", + "ViewAndAddAnnotations": "%sの注釈を表示して追加します…", + "YouCannotModifyThisNote": "あなたが生成したアノテーションではないか、このサイトの管理者権限がないため、このアノテーションは変更できません。" + } +} diff --git a/plugins/Annotations/lang/ka.json b/plugins/Annotations/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/ka.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/ko.json b/plugins/Annotations/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..c4109dbe99a10a164747e3378eb65f96ebb21a76 --- /dev/null +++ b/plugins/Annotations/lang/ko.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "%s에 대한 주석 추가…", + "AnnotationOnDate": "%1$s에 대한 주석: %2$s", + "Annotations": "주석", + "ClickToDelete": "이 주석을 삭제하려면 클릭합니다.", + "ClickToEdit": "이 주석을 편집하려면 클릭합니다.", + "ClickToEditOrAdd": "주석을 편집하거나 추가하거나 생성하려면 클릭합니다.", + "ClickToStarOrUnstar": "클릭하여 주석에 별표 또는 제거합니다.", + "CreateNewAnnotation": "새로운 주석을 생성합니다…", + "EnterAnnotationText": "메모를 입력하세요…", + "HideAnnotationsFor": "%s에 대한 주석 숨기기…", + "IconDesc": "이 기간동안의 메모를 조회합니다.", + "IconDescHideNotes": "이 기간의 노트를 숨깁니다.", + "InlineQuickHelp": "특별한 이벤트를 표시하는 주석을 만들 수 있습니다 (새 블로그 게시물, 또는 웹사이트 재설계 등), 데이터 분석을 저장하거나 중요하다고 생각되는 것들을 저장하세요.", + "LoginToAnnotate": "로그인하여 주석을 생성합니다.", + "NoAnnotations": "이 기간에 해당하는 주석이 없습니다.", + "PluginDescription": "당신의 웹사이트에 변경 사항을 일별로 표시할 수 있는 메모를 첨부 할 수 있습니다. 당신이 가진 생각을 저장하고 동료들과 서로 의견을 나누세요. 당신이 기억해야 할 것들이나 원인과 결과들에 주석을 달아보세요.", + "ViewAndAddAnnotations": "%s을(를) 조회하고 주석을 추가합니다…", + "YouCannotModifyThisNote": "주석을 수정할 수 없습니다. 직접 생성한 주석이 아니거나 이 사이트의 관리 권한이 없기 때문입니다." + } +} diff --git a/plugins/Annotations/lang/ku.json b/plugins/Annotations/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/ku.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/lb.json b/plugins/Annotations/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..649d219caa2ecf1d55acc1909d9c23349396f1d6 --- /dev/null +++ b/plugins/Annotations/lang/lb.json @@ -0,0 +1,16 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Annotatioun fir %s bäifügen…", + "AnnotationOnDate": "Annotatioun iwwer %1$s: %2$s", + "Annotations": "Annotatiounen", + "ClickToDelete": "Klickt fir d'Annotatioun ze läschen.", + "ClickToEdit": "Klickt fir d'Annotatioun ze änneren.", + "ClickToEditOrAdd": "Klickt fir eng Annotatioun ze änneren oder bäizefügen.", + "ClickToStarOrUnstar": "Klickt fir d'Annotatioun bei är Favoriten bäizesetzen/ze läschen.", + "CreateNewAnnotation": "Eng nei Annotatioun erstellen…", + "EnterAnnotationText": "Gëff deng Notiz an…", + "HideAnnotationsFor": "Annotatiounen fir %s net uweisen…", + "IconDesc": "Notizen fir dës Daten Sammlung uweisen.", + "IconDescHideNotes": "Notizen fir dës Daten Sammlung net uweisen." + } +} diff --git a/plugins/Annotations/lang/lt.json b/plugins/Annotations/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..e019f4bd71cfaf0a8959edca84699be4914b3b2c --- /dev/null +++ b/plugins/Annotations/lang/lt.json @@ -0,0 +1,5 @@ +{ + "Annotations": { + "Annotations": "Anotacijos" + } +} diff --git a/plugins/Annotations/lang/lv.json b/plugins/Annotations/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..a5670671db825b9829c090724107202dbe0f1670 --- /dev/null +++ b/plugins/Annotations/lang/lv.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Pievienot %s anotācijas…", + "AnnotationOnDate": "Anotācija %1$s: %2$s", + "Annotations": "Anotācijas", + "ClickToDelete": "Klikšķiniet, lai dzēstu šo anotāciju.", + "ClickToEdit": "Klikšķiet, lai rediģētu šo anotāciju.", + "ClickToEditOrAdd": "Klikšķiet, lai rediģētu vai pievienotu jaunu anotāciju.", + "ClickToStarOrUnstar": "Klikšķiniet, lai pievienotu vai noņemtu zvaigznīti šai anotācijai.", + "CreateNewAnnotation": "Izveidot jaunu anotāciju…", + "EnterAnnotationText": "Ievadiet savu piezīmi…", + "HideAnnotationsFor": "Paslēpt anotācijas %s…", + "IconDesc": "Apskatiet piezīmes šim datumu diapazonam.", + "IconDescHideNotes": "Paslēpt piezīmes šim datumu diapazonam.", + "InlineQuickHelp": "Jūs varat izveidot anotācijas, lai atzīmētu īpašus notikumus (piemēram, jaunu bloga ierakstu vai vietnes pārprojektēšanu), lai saglabātu savus datu analīzes rezultātus vai jebko citu, ko uzskatāt par svarīgu.", + "LoginToAnnotate": "Pieslēdzieties, lai izveidotu anotāciju.", + "NoAnnotations": "Šim datumu diapazonam nav anotāciju.", + "PluginDescription": "Ļauj pievienot piezīmes dažādām dienām, lai atzīmētu tīmekļa vietnē veiktās izmaiņas, saglabātu veiktās datu analīzes un dalītos pārdomās ar kolēģiem. Anotējot datus, jūs noteikti atcerēsities, kāpēc dati izskatās tā, kā tie izskatās.", + "ViewAndAddAnnotations": "Skatīt un pievienot anotācijas %s…", + "YouCannotModifyThisNote": "Jūs nevarat mainīt šo anotāciju, jo neesat to izveidojis, un jums nav administratīvās piekļuves šai vietnei." + } +} diff --git a/plugins/Annotations/lang/ms.json b/plugins/Annotations/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/ms.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/nb.json b/plugins/Annotations/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..4ad912b74ea44404eb80d5fca67a7cbb9591b683 --- /dev/null +++ b/plugins/Annotations/lang/nb.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Legg til merknader for %s…", + "AnnotationOnDate": "Merknad på %1$s: %2$s", + "Annotations": "Merknader", + "ClickToDelete": "Klikk for å slette denne merknaden.", + "ClickToEdit": "Klikk for å redigere denne merknaden.", + "ClickToEditOrAdd": "Klikk for å redigere eller legge til en ny merknad.", + "ClickToStarOrUnstar": "Klikk for å merke eller ikke merke denne merknaden som favoritt.", + "CreateNewAnnotation": "Opprett en ny merknad…", + "EnterAnnotationText": "Skriv inn merknad…", + "HideAnnotationsFor": "Skjul merknader for %s…", + "IconDesc": "Se merknader for dette datoområdet.", + "IconDescHideNotes": "Skjul merknader for denne datoperioden.", + "InlineQuickHelp": "Du kan lage merknader for å markere spesielle hendelser (som et nytt blogginnlegg eller et redesign), for å bruke i dataanalyser eller for å lagre det du mener er viktig.", + "LoginToAnnotate": "Logg inn for å opprette en merknad.", + "NoAnnotations": "Det er ingen merknader i dette datoområdet.", + "PluginDescription": "Lar deg knytte merknader til ulike datoer for å huske endringer på ditt nettsted, lagre tanker du gjør om dine data og dele dine tanker med kollegaer. Ved å merke dine data vil du være sikker på å huske hvorfor dine data ser ut som de gjør.", + "ViewAndAddAnnotations": "Vis og legg til merknader for %s…", + "YouCannotModifyThisNote": "Du kan ikke redigere denne merknaden fordi du ikke laget den, eller du har ikke admintilgang for dette nettstedet." + } +} diff --git a/plugins/Annotations/lang/nl.json b/plugins/Annotations/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..b3858b86bcdb164f52228ce31de7aff10f9c6654 --- /dev/null +++ b/plugins/Annotations/lang/nl.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Notities toevoegen voor %s…", + "AnnotationOnDate": "Notitie op %1$s: %2$s", + "Annotations": "Notities", + "ClickToDelete": "Klik om deze notitie te verwijderen.", + "ClickToEdit": "Klik om deze notitie te bewerken.", + "ClickToEditOrAdd": "Klik om te bewerken of een nieuwe notitie toe te voegen.", + "ClickToStarOrUnstar": "Klik om deze notitie te markeren of niet te markeren.", + "CreateNewAnnotation": "Nieuwe notitie toevoegen…", + "EnterAnnotationText": "Schrijf je notitie…", + "HideAnnotationsFor": "Verberg notities voor %s…", + "IconDesc": "Bekijk notities voor dit datumbereik.", + "IconDescHideNotes": "Verberg notities voor dit datumbereik.", + "InlineQuickHelp": "U kunt notities aanmaken om speciale gebeurtenissen (zoals een nieuw blogbericht of herontwerp website) te markeren, om gegevensanalyses op te slaan of iets anders dat u belangrijk vindt op te slaan.", + "LoginToAnnotate": "Log in om een notitie aan te maken.", + "NoAnnotations": "Er zijn geen notities voor dit datumbereik.", + "PluginDescription": "Maakt het mogelijk om opmerkingen op verschillende dagen bij te voegen om wijzigingen aan uw website te markeren, analyses die u maakt met betrekking tot uw gegevens op te slaan en uw gedachten met collega's te delen. Door notities bij uw gegevens te plaatsen, zorgt u ervoor dat u weet waarom uw gegevens er op een bepaalde manier uitzien.", + "ViewAndAddAnnotations": "Bekijk en voeg notities toe voor %s…", + "YouCannotModifyThisNote": "U kunt deze notitie niet aanpassen, omdat u deze niet heeft aangemaakt en u heeft ook geen beheerdersrechten voor deze site." + } +} diff --git a/plugins/Annotations/lang/nn.json b/plugins/Annotations/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/nn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/pl.json b/plugins/Annotations/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..7e02dd178a8e747eacb361b8ad516f013f0d74ec --- /dev/null +++ b/plugins/Annotations/lang/pl.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Dodaj adnotacje dla %s…", + "AnnotationOnDate": "Adnotacja do %1$s: %2$s", + "Annotations": "Adnotacje", + "ClickToDelete": "Kliknij aby usunąć tą adnotację.", + "ClickToEdit": "Kliknij aby edytować tą adnotację.", + "ClickToEditOrAdd": "Kliknij aby edytować albo dodać nową adnotację.", + "ClickToStarOrUnstar": "Kliknij aby zaznaczyć lub odznaczyć (gwiazdką) tą adnotację.", + "CreateNewAnnotation": "Utwórz nową adnotację…", + "EnterAnnotationText": "Wpisz swoją notatkę…", + "HideAnnotationsFor": "Ukryj tą adnotację dla %s…", + "IconDesc": "Pokarz notatki dla danego zakresu dat.", + "IconDescHideNotes": "Ukryj notatki dla tego zakresu dat.", + "InlineQuickHelp": "Możesz utworzyć adnotację aby zaznaczać specjalne zdarzenia (jak nowy wpis na blogu albo re-design strony) aby zapisać twoje dane analityczne lub by zapisać cokolwiek innego co wydaje ci się ważne.", + "LoginToAnnotate": "Zaloguj się aby utworzyć adnotację.", + "NoAnnotations": "Nie ma adnotacji dla tego zakresu dat.", + "PluginDescription": "Daje ci możliwość dodania notatek do innych dni aby zaznaczyć zmiany na twojej stronie, zapisać jakiś wnioski dotyczące twoich danych oraz podzielić się twoimi spostrzeżeniami z kolegami. Adnotując twoje dane będziesz miał pewność że pamiętasz dlaczego twoje dane wyglądają tak jak wyglądają.", + "ViewAndAddAnnotations": "Pokarz i dodaj adnotację dla %s…", + "YouCannotModifyThisNote": "Nie możesz modyfikować tej adnotacji ponieważ jej nie utworzyłeś lub nie masz praw administratora dla tej strony." + } +} diff --git a/plugins/Annotations/lang/pt-br.json b/plugins/Annotations/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..e3a778884363f8fec847f9f9a3aebcd8d30ad67f --- /dev/null +++ b/plugins/Annotations/lang/pt-br.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Adicione anotações para %s…", + "AnnotationOnDate": "Anotação em %1$s: %2$s", + "Annotations": "Anotações", + "ClickToDelete": "Clique para excluir esta anotação.", + "ClickToEdit": "Clique para editar esta anotação.", + "ClickToEditOrAdd": "Clique para editar ou adicione uma nova anotação.", + "ClickToStarOrUnstar": "Clique para adicionar ou remover uma estrela a esta anotação.", + "CreateNewAnnotation": "Criar uma nova anotação…", + "EnterAnnotationText": "Digite a sua nota…", + "HideAnnotationsFor": "Esconder anotações para %s…", + "IconDesc": "Ver notas para este intervalo de datas.", + "IconDescHideNotes": "Esconder anotações para este intervalo de datas.", + "InlineQuickHelp": "Você pode criar anotações para marcar eventos especiais (como uma nova postagem no blog, ou redefinição do site), para salvar suas análises de dados ou para salvar qualquer coisa que você acha que é importante.", + "LoginToAnnotate": "Faça login para criar uma anotação.", + "NoAnnotations": "Não existem notas para este intervalo de datas.", + "PluginDescription": "Permite anexar notas para dias diferentes para marcar as alterações feitas em seu site, salvar as análises que você faz a respeito de seus dados e compartilhar suas opiniões com seus colegas. Anotando seus dados, você poderá se lembrar por que seus dados possuem determinada aparência no sistema.", + "ViewAndAddAnnotations": "Ver e adicionar notas para %s…", + "YouCannotModifyThisNote": "Você não pode modificar essa anotação, porque você não possui permição, nem possui acesso de administrador para este site." + } +} diff --git a/plugins/Annotations/lang/pt.json b/plugins/Annotations/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..55c5a13afd5ee7663ffa210439fac93bf73a3dba --- /dev/null +++ b/plugins/Annotations/lang/pt.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Adicionar anotações para %s…", + "AnnotationOnDate": "Anotação em %1$s: %2$s", + "Annotations": "Anotações", + "ClickToDelete": "Clique para eliminar esta anotação.", + "ClickToEdit": "Clique para editar esta anotação.", + "ClickToEditOrAdd": "Clique para editar ou adicionar uma nova anotação.", + "ClickToStarOrUnstar": "Clique para adicionar ou remover favorito desta anotação.", + "CreateNewAnnotation": "Criar uma nova anotação…", + "EnterAnnotationText": "Insira a sua nota…", + "HideAnnotationsFor": "Ocultar anotações para %s…", + "IconDesc": "Ver notas para este intervalo de datas.", + "IconDescHideNotes": "Ocultar notas para este intervalo de datas.", + "InlineQuickHelp": "Pode criar anotações para marcar eventos especiais (tais como uma nova publicação no blogue, ou redesenho do site), para guardar as suas análises de dados ou para guardar qualquer outra coisa que considere ser importante.", + "LoginToAnnotate": "Inicie a sessão para criar uma anotação.", + "NoAnnotations": "Não existem anotações para este intervalo de datas.", + "PluginDescription": "Permite que anexe notas a dias diferentes para marcar alterações realizadas ao seu site, guardar análises que realizou relativas aos seus dados e partilhar os seus pensamentos com os seus colegas. Ao anotar a sua informação, terá a certeza que se lembrará da razão pela qual esta tem o aspeto que tem.", + "ViewAndAddAnnotations": "Ver e adicionar anotações para %s…", + "YouCannotModifyThisNote": "Não pode modificar esta anotação, porque não a criou, nem tem acesso de administrador para este site." + } +} diff --git a/plugins/Annotations/lang/ro.json b/plugins/Annotations/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..d9c6425df14bfc86abc9c8b468215ade4c5f0b1d --- /dev/null +++ b/plugins/Annotations/lang/ro.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Adaugă notiţe pentru %s…", + "AnnotationOnDate": "Comentariu in %1$s: %2$s", + "Annotations": "Note", + "ClickToDelete": "Click pentru a sterge acesta nota", + "ClickToEdit": "Dă clic pentru a modifica aceasă notiţă.", + "ClickToEditOrAdd": "Dă click pentru a edita sau a adăuga o notiţă.", + "ClickToStarOrUnstar": "Click pentru a marca sau demarca aceasta nota.", + "CreateNewAnnotation": "Crează o notiţă nouă…", + "EnterAnnotationText": "Introdu nota…", + "HideAnnotationsFor": "Ascunde adnotaiile pentru %s…", + "IconDesc": "Vezi notele pentru aceasta data de interval.", + "IconDescHideNotes": "Ascunde notiţele pentru acest interval", + "InlineQuickHelp": "Poti crea note pentru a marca evenimente speciale (cum ar fi o noua postare pe blog sau redesign-ul siteului), pentru a salva datele analizate sau pentru a salva orice altceva crezi ca este important.", + "LoginToAnnotate": "Conecteaza-te pentru a crea o nota.", + "NoAnnotations": "Nu există notiţe pentru acest interval", + "PluginDescription": "Permite sa inserezi note pentru zile diferite pentru a marca modificarile facute la siteul tau, salveaza rapoartele pe care le faci privind datele siteului tau si impartaseste gandurile cu colegii. Facand note pentru datele tale, vei fi sigur ca iti vei aminti de ce datele tale arata in felul in care sunt.", + "ViewAndAddAnnotations": "Vezi şi adaugă notiţe pentru %s…", + "YouCannotModifyThisNote": "Nu poti modifica aceasta nota deoarece nu ai creat-o tu sau pentru ca nu ai access ca admin pentru acest site." + } +} diff --git a/plugins/Annotations/lang/ru.json b/plugins/Annotations/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..d17a6398037db8b241eae71d08e66691904824d2 --- /dev/null +++ b/plugins/Annotations/lang/ru.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Добавить примечания для %s…", + "AnnotationOnDate": "Примечание на %1$s: %2$s", + "Annotations": "Примечания", + "ClickToDelete": "Удалить примечание.", + "ClickToEdit": "Редактировать примечание.", + "ClickToEditOrAdd": "Изменить или добавить новые примечания.", + "ClickToStarOrUnstar": "Поставить или снять примечание.", + "CreateNewAnnotation": "Создать новое примечание…", + "EnterAnnotationText": "Введите свою заметку…", + "HideAnnotationsFor": "Спрятать примечания для %s…", + "IconDesc": "Просмотреть заметки для данного диапазона дат.", + "IconDescHideNotes": "Спрятать заметки для данного диапазона дат.", + "InlineQuickHelp": "Вы можете создавать примечания для того, чтобы отмечать важные события в истории вашего сайта (такие как новый пост в блоге или редизайн).", + "LoginToAnnotate": "Войдите для создания примечаний.", + "NoAnnotations": "Нет примечаний в данном диапазоне дат.", + "PluginDescription": "Позволяет прикрепить заметки к различным дням, чтобы отметить изменения, внесённые в ваш сайт. Создавая примечания вы будете знать, почему посещения изменились именно так.", + "ViewAndAddAnnotations": "Просмотреть и добавить примечания для %s…", + "YouCannotModifyThisNote": "Вы не можете изменить это примечание, так как вы не являетесь её автором или у вас нет прав администратора." + } +} diff --git a/plugins/Annotations/lang/si.json b/plugins/Annotations/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/sk.json b/plugins/Annotations/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..9c60a2b735d2269e040ddd4b54071c7cc053f984 --- /dev/null +++ b/plugins/Annotations/lang/sk.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Pridať anotáciu k %s…", + "AnnotationOnDate": "Anotácia na %1$s: %2$s", + "Annotations": "Anotácie", + "ClickToDelete": "Kliknutím odstránite túto anotáciu.", + "ClickToEdit": "Kliknutím upravíte túto anotáciu.", + "ClickToEditOrAdd": "Klknutím upravíte alebo pridáte anotáciu.", + "ClickToStarOrUnstar": "Klknutím označíte alebo odznačíte hviezdičkou anotáciu.", + "CreateNewAnnotation": "Vytvoriť novú anotáciu…", + "EnterAnnotationText": "Vložte svoju poznámku…", + "HideAnnotationsFor": "Skryť anotáciu pre %s…", + "IconDesc": "Pozrieť poznámky pre tento časový rozsah.", + "IconDescHideNotes": "Skryť poznámky pre tento časový rozsah.", + "InlineQuickHelp": "Môžete vytvoriť anotácie na onačenie špeciálnych udalostí (ako nový príspevok na blogu, alebo redizajn webovej stránky), na uloženie Vašich dátových analýz alebo na uloženie čohokoľvek ďalšieho čo si myslíte, že je dôležité.", + "LoginToAnnotate": "Prihláste sa pre vytvorenie anotácie.", + "NoAnnotations": "Pre tento časový rozsah nie sú anotácie.", + "PluginDescription": "Dovoľuje vám pridať poznámky k jednotlivým dňom na označenie zmien na stránke, zaznamenanie analýz vašich dát a zdieľanie myšlienok s kolegami. Anotovaním vašich dát zaistíte, že v budúcnosti budete vedieť, prečo vaše dáta vyzerajú tak, ako vyzerajú.", + "ViewAndAddAnnotations": "Zobraziť a pridať anotácie pre %s…", + "YouCannotModifyThisNote": "Nemôžete upraviť túto anotáciu, pretože ste ju nevytvorili, ani nemáte administrátorský prístup pre túto stránku." + } +} diff --git a/plugins/Annotations/lang/sl.json b/plugins/Annotations/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..b55e1a9a9553247ad4290687deee2c8e0e1318df --- /dev/null +++ b/plugins/Annotations/lang/sl.json @@ -0,0 +1,14 @@ +{ + "Annotations": { + "Annotations": "Beležke", + "ClickToDelete": "Kliknite za izbris te beležke.", + "ClickToEdit": "Kliknite za urejanje te beležke.", + "ClickToEditOrAdd": "Kliknite za urejanje ali dodajanje nove beležke.", + "EnterAnnotationText": "Dodajte svojo opombo…", + "HideAnnotationsFor": "Skrij opombe za %s…", + "IconDesc": "Preglej opombe za ta časovni razpon.", + "IconDescHideNotes": "Skrij opombe za ta časovni razpon.", + "LoginToAnnotate": "Za kreiranje beležke se morate prijaviti.", + "ViewAndAddAnnotations": "Preglej in dodaj opombe za %s…" + } +} diff --git a/plugins/Annotations/lang/sq.json b/plugins/Annotations/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..5f4894b4ca7f4c20d2df700be484084354e30843 --- /dev/null +++ b/plugins/Annotations/lang/sq.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Shtoni shënime për %s…", + "AnnotationOnDate": "Shënim te %1$s: %2$s", + "Annotations": "Shënime", + "ClickToDelete": "Klikoni që të fshihet ky shënim.", + "ClickToEdit": "Klikoni që të përpunohet ky shënim.", + "ClickToEditOrAdd": "Klikoni që të përpunohet, ose që të shtohet një shënim i ri.", + "ClickToStarOrUnstar": "Klikoni që këtij shënimi t’i vihet ose hiqet ylli.", + "CreateNewAnnotation": "Krijoni një shënim të ri…", + "EnterAnnotationText": "Jepni shënimin tuaj…", + "HideAnnotationsFor": "Fshihi shënimet për %s…", + "IconDesc": "Shihni shënimet për këtë interval datash.", + "IconDescHideNotes": "Fshihi shënimet për këtë interval datash.", + "InlineQuickHelp": "Mund të krijoni shënime për të vënë një shenjë te akt special (fjala vjen, një postim i ri blogu, ose rihartim sajti), për të ruajtur analiza të të dhënave tuaja ose për të ruajtur çfarëdo gjëje tjetër që mendoni se është e rëndësishme.", + "LoginToAnnotate": "Që të krijoni një shënim, bëni hyrjen.", + "NoAnnotations": "S’ka shënime për këtë interval datash.", + "PluginDescription": "Ju lejon të bashkëngjitni shënime në ditë të ndryshme, për t’u vënë shenjë ndryshimeve të bëra te sajti juaj, të ruani analiza që bëni dhe që lidhen me të dhënat tuaja, si dhe për të ndarë me kolegët mendimet tuaja. Duke bërë shënime në të dhënat tuaja, do të bëni të mundur të mbani mend pse të dhënat tuaja duken ashtu si duken.", + "ViewAndAddAnnotations": "Shihni dhe shtoni shënime për %s…", + "YouCannotModifyThisNote": "S’e ndryshoni dot këtë shënim, ngaqë s’e krijuat ju, as keni të drejta përgjegjësi për këtë sajt." + } +} diff --git a/plugins/Annotations/lang/sr.json b/plugins/Annotations/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..519113b3416657bb698c58fed4769c7e93f1f4be --- /dev/null +++ b/plugins/Annotations/lang/sr.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Dodaj belešku za %s…", + "AnnotationOnDate": "Beleške za %1$s: %2$s", + "Annotations": "Beleške", + "ClickToDelete": "Kliknite kako biste obrisali belešku.", + "ClickToEdit": "Kliknite kako biste izmenili belešku.", + "ClickToEditOrAdd": "Kliknite kako biste dodali ili izmenili belešku.", + "ClickToStarOrUnstar": "Kliknite kako biste markirali belešku.", + "CreateNewAnnotation": "Napravi novu belešku…", + "EnterAnnotationText": "Upišite belešku…", + "HideAnnotationsFor": "Sakrij beleške za %s…", + "IconDesc": "Prikaži beleške za ovaj vremenski period.", + "IconDescHideNotes": "Sakrij beleške za ovaj vremenski period.", + "InlineQuickHelp": "Možete da kreirate beleške kako biste istakli posebne događaje (novi post na blogu, redizajn sajta), sačuvali vašu analizu podataka ili kako biste sačuvali bilo šta drugo što smatrate važnim.", + "LoginToAnnotate": "Prijavite se kako biste napravili novu belešku.", + "NoAnnotations": "Nema beleški za ovaj vremenski period.", + "PluginDescription": "Omogućuje vam da dodate beleške za određene dane kako biste zapamtili zašto podaci izgledaju tako kako izgledaju.", + "ViewAndAddAnnotations": "Prikaži i dodaj beleške za %s…", + "YouCannotModifyThisNote": "Ovu belešku ne možete da izmenite zato što je niste vi kreirali i nemate nivo administratorskog pristupa sajtu." + } +} diff --git a/plugins/Annotations/lang/sv.json b/plugins/Annotations/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..96a5fd8aa1f21d57388bf414931a4c2f57b1da86 --- /dev/null +++ b/plugins/Annotations/lang/sv.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Lägg till en anteckning för %s…", + "AnnotationOnDate": "Anteckning den %1$s: %2$s", + "Annotations": "Anteckningar", + "ClickToDelete": "Klicka för att ta bort denna anteckning.", + "ClickToEdit": "Klicka för att redigera denna anteckning.", + "ClickToEditOrAdd": "Klicka för att redigera eller lägga till en ny anteckning.", + "ClickToStarOrUnstar": "Klicka för att stjärnmärka eller ta bort stjärnmärkning för denna anteckning.", + "CreateNewAnnotation": "Skapa ny anteckning…", + "EnterAnnotationText": "Skriv in din anteckning…", + "HideAnnotationsFor": "Dölj anteckning för %s…", + "IconDesc": "Visa anteckningar för det här datumintervallet.", + "IconDescHideNotes": "Dölj anteckningar för det här datumintervallet.", + "InlineQuickHelp": "Du kan skapa anteckningar för att markera speciella händelser (som t.ex. ett nytt blogginlägg eller ny design av webbplatsen), för att spara data-analyser eller annat som du tycker är viktigt.", + "LoginToAnnotate": "Logga in för att skapa en anteckning.", + "NoAnnotations": "Det finns inga noteringar för detta datumintervall.", + "PluginDescription": "Tillåter dig att koppla anteckningar till olika datum för att markera ändringar som gjorts till webbplatsen, spara analyser som du gör av din data och dela dina tankar med kollegor. Genom att koppla anteckningar så kommer du alltid komma ihåg vad som gör att din data förändras och ser ut som den gör.", + "ViewAndAddAnnotations": "Visa och lägg till anteckningar för %s…", + "YouCannotModifyThisNote": "Du kan inte ändra den här anteckningen eftersom du inte har skapat den, eller har admin-behörighet för den här webbplasten." + } +} diff --git a/plugins/Annotations/lang/ta.json b/plugins/Annotations/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..931e3e49f6a2bcf6e1cf10cff789db299faa7e5f --- /dev/null +++ b/plugins/Annotations/lang/ta.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "விளக்கங்களை சேர்க்க %s…", + "AnnotationOnDate": "விளக்கம் %1$s: %2$s", + "Annotations": "சிறுகுறிப்புகள்", + "ClickToDelete": "சிறுகுறிப்பை நீக்க இங்கே சொடுக்கவும்.", + "ClickToEdit": "சிறுகுறிப்பை மாற்ற இங்கே சொடுக்கவும்.", + "ClickToEditOrAdd": "சிறுகுறிப்பை மாற்ற அல்லது புதிதாகச் சேர்க்க இங்கே சொடுக்கவும்.", + "ClickToStarOrUnstar": "இந்த சிறுகுறிப்பை நட்சத்திரக் குறிஇட அல்லது நட்சத்திரக் குறியை நீக்க இங்கே சொடுக்கவும்.", + "CreateNewAnnotation": "ஒரு புதிய சிறுகுறிப்பை உருவாக்கவும்…", + "EnterAnnotationText": "உங்கள் குறிப்பை உள்ளிடவும்…", + "HideAnnotationsFor": "%sக்கான சிறுகுறிப்புகளை மறைக்கவும்…", + "IconDesc": "இந்தத் தேதி வரம்பிற்கான குறிப்புகளைக் காட்டு.", + "IconDescHideNotes": "இந்தத் தேதி வரம்பிற்கான குறிப்புகளை மறை.", + "InlineQuickHelp": "சிறப்பு நிகழ்வுகளை (புதிய வலைப்பதிவு இடுகை அல்லது வலைத்தள மறுவடிவமைப்பு போன்றவை) குறிக்க, உங்கள் தரவு பகுப்பாய்வுகளைச் சேமிக்க அல்லது முக்கியமானது என்று நீங்கள் நினைக்கும் எதையும் சேமிக்க சிறுகுறிப்புகளை உருவாக்கலாம்.", + "LoginToAnnotate": "சிறுகுறிப்பை உருவாக்க உள்நுழைக.", + "NoAnnotations": "இந்த தேதி வரம்பிற்கு எந்த சிறுகுறிப்பும் இல்லை.", + "PluginDescription": "உங்கள் வலைத்தளத்தில் செய்யப்பட்ட மாற்றங்களைக் குறிக்க, உங்கள் தரவைப் பற்றி நீங்கள் செய்த பகுப்பாய்வுகளைச் சேமிக்கவும், உங்கள் எண்ணங்களை உங்கள் சகாக்களுடன் பகிர்ந்து கொள்ளவும் வெவ்வேறு நாட்களில் குறிப்புகளை இணைக்க உங்களை அனுமதிக்கிறது. உங்கள் தரவை குறிப்பதன் மூலம், உங்கள் தரவு ஏன் இவ்வாறு தோற்றமளிக்கிறது என்பதை நினைவில் கொண்டு உறுதி செய்வீர்கள்.", + "ViewAndAddAnnotations": "%sக்கு விளக்கங்களை சேர்க்கவும் பார்க்கவும்…", + "YouCannotModifyThisNote": "இந்தச் சிறுகுறிப்பை உங்களால் மாற்ற முடியாது. என் என்றால் நீங்கள் இதை உருவாக்கவில்லை அல்லது உங்களுக்கு நிர்வாகி உரிமைகள் இல்லை." + } +} diff --git a/plugins/Annotations/lang/te.json b/plugins/Annotations/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..884a3a85ddf86da9f40e383743762b0b0ce84804 --- /dev/null +++ b/plugins/Annotations/lang/te.json @@ -0,0 +1,5 @@ +{ + "Annotations": { + "ClickToEdit": "మార్చడానికి నొక్కండి" + } +} diff --git a/plugins/Annotations/lang/th.json b/plugins/Annotations/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..935089e0aab0b25aa2ef81e8f188cca6eeeed50d --- /dev/null +++ b/plugins/Annotations/lang/th.json @@ -0,0 +1,7 @@ +{ + "Annotations": { + "Annotations": "หมายเหตุประกอบ", + "EnterAnnotationText": "กรอกหมายเหตุของคุณ…", + "LoginToAnnotate": "เข้าสู่ระบบเพื่อสร้างหมายเหตุประกอบ" + } +} diff --git a/plugins/Annotations/lang/tl.json b/plugins/Annotations/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..c53f6418b234347e49afac6a57184cf690aefbbd --- /dev/null +++ b/plugins/Annotations/lang/tl.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Magdagdag ng mga anotasyon para sa %s…", + "AnnotationOnDate": "Anotasyon sa %1$s: %2$s", + "Annotations": "Mga anotasyon", + "ClickToDelete": "I-Click upang alisin ang anotasyong ito", + "ClickToEdit": "I-Click upang baguhin ang anotasyong ito", + "ClickToEditOrAdd": "I-Click upang baguhin o dagdagan ang anotasyong ito", + "ClickToStarOrUnstar": "I-Click upang lagyan ng bituin o hindi ang anotasyong ito", + "CreateNewAnnotation": "Gumawa ng bagong anotasyon…", + "EnterAnnotationText": "Ipasok ang iyong paalala…", + "HideAnnotationsFor": "Itago ang mga anotasyon para sa %s…", + "IconDesc": "Tingnan ang mga paalala para sa hanay ng petsa na ito.", + "IconDescHideNotes": "Itago ang mga paalala para sa hanay ng petsa na ito.", + "InlineQuickHelp": "Maaari kang lumikha ng mga anotasyon upang markahan ang mga espesyal na mga kaganapan (tulad ng isang bagong post sa blog, o pagbabago ng disenyo ng website), upang i-save ang iyong pag-aaral ng datos o i-save ang anumang bagay na sa tingin mo ay mahalaga.", + "LoginToAnnotate": "Mag-login upang gumawa ng isang anotasyon.", + "NoAnnotations": "Walang mga anotasyon para sa hanay ng petsa na ito.", + "PluginDescription": "Pinapayagan kang mag-attach ng mga tala sa iba't ibang araw upang markahan ang mga pagbabagong ginawa sa iyong website, i-save ang pinag-aaralan na gagawin mo tungkol sa iyong datos at ibahagi ang iyong mga pananaw sa iyong mga kasamahan. Sa pamamagitan ng annotating ng iyong data, tiyak na iyong maaalala kung bakit ganito ang itsura ng iyong mga datos.", + "ViewAndAddAnnotations": "Tingnan at magdagdag ng mga anotasyon para sa %s…", + "YouCannotModifyThisNote": "Hindi mo maaaring baguhin ang anotasyon na ito, dahil ito ay hindi mo nilikha, at wala ka ring admin access para sa site na ito." + } +} diff --git a/plugins/Annotations/lang/tr.json b/plugins/Annotations/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..ef79be18116d8a27e37dc8f73146c940607bfc25 --- /dev/null +++ b/plugins/Annotations/lang/tr.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "%s için notlar ekle…", + "AnnotationOnDate": "%1$s için not: %2$s", + "Annotations": "Notlar", + "ClickToDelete": "Bu notu silmek için tıklayın.", + "ClickToEdit": "Bu notu düzenlemek için tıklayın.", + "ClickToEditOrAdd": "Not eklemek ya da düzenlemek icin buraya tıklayın.", + "ClickToStarOrUnstar": "Bu nota işaret koymak ya da kaldırmak için tıklayın.", + "CreateNewAnnotation": "Not ekle…", + "EnterAnnotationText": "Notunuzu yazın…", + "HideAnnotationsFor": "%s için notları gizle…", + "IconDesc": "Bu tarih aralığındaki notları görüntüle.", + "IconDescHideNotes": "Bu tarih aralığındaki notları gizle.", + "InlineQuickHelp": "Önemli etkinlikleri (yeni blog iletileri ya da site tasarım değişiklikleri gibi), veri incelemelerinizi ya da önemli olduğunu düşündüğünüz konuları kaydetmek için notlar ekleyebilirsiniz.", + "LoginToAnnotate": "Not eklemek için oturum açın.", + "NoAnnotations": "Bu tarih aralığında bir not yok.", + "PluginDescription": "Bu özellik sitede farklı günlerde yaptığınız değişiklikleri veri incelemeleri ve düşüncelerinizi çalışma arkadaşlarınızla paylaşmak için notlar eklenmesini sağlar. Verilerinize not ekleyerek neden bu şekilde görüntülendiklerini hatırlayabilirsiniz.", + "ViewAndAddAnnotations": "%s için notları görüntüle ve yeni not ekle…", + "YouCannotModifyThisNote": "Bu notu siz eklemediğiniz ya da yönetici yetkileriniz olmadığı için düzenleyemezsiniz." + } +} diff --git a/plugins/Annotations/lang/tzm.json b/plugins/Annotations/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Annotations/lang/tzm.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Annotations/lang/uk.json b/plugins/Annotations/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..455d80be2fbb130f4bc7d3fa3365fb91d6ee309c --- /dev/null +++ b/plugins/Annotations/lang/uk.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Додати примітки для %s…", + "AnnotationOnDate": "Примітка на %1$s: %2$s", + "Annotations": "Примітки", + "ClickToDelete": "Видалити цю примітку.", + "ClickToEdit": "Редагувати примітку.", + "ClickToEditOrAdd": "Змінити або додати нові примітки.", + "ClickToStarOrUnstar": "Поставити або зняти позначку.", + "CreateNewAnnotation": "Створити нову примітку…", + "EnterAnnotationText": "Введіть свою примітку…", + "HideAnnotationsFor": "Сховати примітки для %s…", + "IconDesc": "Переглянути примітки для цього діапазону дат.", + "IconDescHideNotes": "Сховати примітки для цього діапазону дат.", + "InlineQuickHelp": "Ви можете створювати примітки, щоб позначати важливі події в історії вашого сайту (такі як новий допис у блозі або редизайн).", + "LoginToAnnotate": "Увійдіть для створення приміток.", + "NoAnnotations": "Немає приміток у цьому діапазоні дат.", + "PluginDescription": "Дозволяє прикріплювати примітки до різних днів, щоб позначати зміни, внесені на вашому вебсайті, зберігати аналіз ваших даних і ділитися своїми думками з колегами. Коментуючи свої дані, ви будете впевнені, що пам'ятаєте, чому ваші дані виглядають саме так, а не інакше.", + "ViewAndAddAnnotations": "Переглянути та додати примітки для %s…", + "YouCannotModifyThisNote": "Ви не можете змінити цю примітку, оскільки ви не її автор. Також у вас немає прав адміністратора." + } +} diff --git a/plugins/Annotations/lang/ur.json b/plugins/Annotations/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..8bdbc18827e1f5a23c33bfe6c56c3f7fee4f01ea --- /dev/null +++ b/plugins/Annotations/lang/ur.json @@ -0,0 +1,11 @@ +{ + "Annotations": { + "Annotations": "تشریحات", + "ClickToEdit": "اس تشریح کی ترمیم کیلئے کلک کریں۔", + "ClickToStarOrUnstar": "اس تشریح کو سٹار یا ان سٹار کرنے کیلئے کلک کریں۔", + "EnterAnnotationText": "اپنے نوٹ لکھیں…", + "IconDesc": "اس تاریخی حدود کے نوٹس دیکھیے۔", + "LoginToAnnotate": "تشریح لکھنے کیلیے لاگ ان ہوں۔", + "ViewAndAddAnnotations": "%s کیلیے تشریح لکھیے اور دیکھیے…" + } +} diff --git a/plugins/Annotations/lang/vi.json b/plugins/Annotations/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..5acc08f0c8f070109f5700240342469ca439e378 --- /dev/null +++ b/plugins/Annotations/lang/vi.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "Thêm các chú thích cho %s…", + "AnnotationOnDate": "Chú thích trên %1$s: %2$s", + "Annotations": "Các chú thích", + "ClickToDelete": "Click để xóa chú thích này", + "ClickToEdit": "Click để sửa chú thích này", + "ClickToEditOrAdd": "Click để sửa hay thêm một chú thích mới", + "ClickToStarOrUnstar": "Click để đánh dấu sao hay bỏ đánh dấu chú thích này", + "CreateNewAnnotation": "Tạo một chú thích mới…", + "EnterAnnotationText": "Nhập ghi chú của bạn…", + "HideAnnotationsFor": "Ẩn các chú thích cho %s…", + "IconDesc": "Xem ghi chú trong giới hạn của ngày này", + "IconDescHideNotes": "Ẩn ghi chú trong giới hạn của ngày này", + "InlineQuickHelp": "Bạn có thể tạo chú thích để đánh dấu những sự kiện quan trọng (như là: thời điểm đăng một bài blog mới hoặc thay đổi thiết kế trang web), để ghi nhớ những phân tích dữ liệu hoặc để ghi lại bất cứ gì bạn cho là quan trọng.", + "LoginToAnnotate": "Đăng nhập để tạo một chú thích", + "NoAnnotations": "Không có chú thích nào trong phạm vi ngày này", + "PluginDescription": "Cho phép bạn đính kèm ghi chú vào những ngày khác nhau; đánh dấu những thay đổi đã thực hiện trên trang web của bạn; ghi nhớ những phân tích bạn thực hiện liên quan đến dữ liệu của bạn; và chia sẻ quan điểm của bạn với đồng nghiệp. Việc chú thích sẽ giải thích cho bạn vì sao dữ liệu của bạn đang ở tình trạng hiện tại.", + "ViewAndAddAnnotations": "Hiện thị và thêm chú thích cho %s…", + "YouCannotModifyThisNote": "Bạn không thể sửa đổi chú thích này, bởi vì bạn đã không tạo ra nó, bạn cũng không có quyền quản trị trang web này." + } +} diff --git a/plugins/Annotations/lang/zh-cn.json b/plugins/Annotations/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..36b93a34702e892e7c3a33e0b0cb703818f72f98 --- /dev/null +++ b/plugins/Annotations/lang/zh-cn.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "为 %s 添加注释…", + "AnnotationOnDate": "%1$s的备注:%2$s", + "Annotations": "备注", + "ClickToDelete": "点击删除此备注。", + "ClickToEdit": "点击编辑此备注。", + "ClickToEditOrAdd": "点击编辑或新增备注。", + "ClickToStarOrUnstar": "点击为此备注增加或取消星标。", + "CreateNewAnnotation": "创建新注释…", + "EnterAnnotationText": "输入您的注释…", + "HideAnnotationsFor": "隐藏 %s 的注释…", + "IconDesc": "查看此时间段内的备注。", + "IconDescHideNotes": "隐藏此时间段内的备注。", + "InlineQuickHelp": "您可以添加备注用于记录特别事件(例如新的博客文章,或者网站重新设计),保存您的数据分析或任何您觉得重要的事情。", + "LoginToAnnotate": "登录以添加备注。", + "NoAnnotations": "该时间段内没有备注。", + "PluginDescription": "可以添加说明文字到不同的日期来标记您网站的修改,保存您关于数据的分析并与同事分享您的想法。通过备注资料,您能确保记住为什么您的数据看起来是这样的。", + "ViewAndAddAnnotations": "查看并添加 %s 的注释…", + "YouCannotModifyThisNote": "您无法修改此备注,因为这个备注不是您创建的,并且您也没有此网站的管理员权限。" + } +} diff --git a/plugins/Annotations/lang/zh-tw.json b/plugins/Annotations/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..bd95b5dd856fa47c28ebad85c46790bc4e6bcd59 --- /dev/null +++ b/plugins/Annotations/lang/zh-tw.json @@ -0,0 +1,22 @@ +{ + "Annotations": { + "AddAnnotationsFor": "新增 %s 的註解…", + "AnnotationOnDate": "%1$s 的註解:%2$s", + "Annotations": "註解", + "ClickToDelete": "點擊刪除此註解。", + "ClickToEdit": "點擊編輯此註解。", + "ClickToEditOrAdd": "點擊以編輯或新增註解。", + "ClickToStarOrUnstar": "點擊以標示或撤銷此註解的星號。", + "CreateNewAnnotation": "新增註解…", + "EnterAnnotationText": "輸入你的筆記…", + "HideAnnotationsFor": "隱藏 %s 的註解…", + "IconDesc": "查看此日期範圍的筆記。", + "IconDescHideNotes": "隱藏此日期範圍的筆記。", + "InlineQuickHelp": "你可以建立註解來標記特殊事件(如部落格新文章或網頁重新設計),來保存你的資料分析或是任何你覺得重要的事。", + "LoginToAnnotate": "登入以建立註解。", + "NoAnnotations": "這個日期範圍內沒有註解。", + "PluginDescription": "讓你可以在不同日期加入筆記來標記你的網站修改處,保存你的分析數據資料以及和你的同事分享你的想法。透過註解,你可以確保未來能記得為什麼你的資料看起來會是這樣。", + "ViewAndAddAnnotations": "查看並新增 %s 的註解…", + "YouCannotModifyThisNote": "你無法編輯這個註解,因為你並沒有建立它,也沒有這個網站的管理權限。" + } +} diff --git a/plugins/Annotations/stylesheets/annotations.less b/plugins/Annotations/stylesheets/annotations.less new file mode 100644 index 0000000000000000000000000000000000000000..4f69db3408f53f1dcbdd2027f07c9d35f1a6296f --- /dev/null +++ b/plugins/Annotations/stylesheets/annotations.less @@ -0,0 +1,244 @@ +.evolution-annotations { + position: relative; + height: 18px; + width: 100%; + cursor: pointer; + margin-top: 3px; + + .icon-annotation { + font-size: 16px; + color: #666666; + } + + .icon-annotation.starred { + color: @theme-color-brand; + } + + & > span { + position: absolute; + } +} + +.annotation-manager { + text-align: left; + margin-top: 30px; + + .new-annotation-row { + height: 145px; + .input-field { + margin-top: 2px; + } + } + + .annotation.edit .annotation-edit-mode { + min-height: 125px; + + .input-field { + margin-top: 2px; + } + } + + .input-field { + margin-bottom: 0; + input { + height: 1rem; + padding-bottom: 4px; + } + } +} + +span.annotation { + display: block; + font-size: 20px; + color: @theme-color-text-highContrast; + font-style: normal; + text-align: left; + padding-left: 10px; +} + +.annotations-header { + display: inline-block; + width: 128px; + text-align: left; + font-size: 12px; + font-style: italic; + margin-bottom: 8px; + vertical-align: top; + color: @theme-color-text-lighter; +} + +.annotation-controls { + display: inline-block; + margin:0; + padding: 50px 0 10px 10px; + color: transparent; +} + +.annotation-controls > a { + font-size:14px; + font-style: normal; + color: @theme-color-text-highContrast; + cursor: pointer; + padding: 3px 0 6px 0; + display: inline-block; + margin:0; + text-decoration: none; +} + +.annotation-controls > a:hover { + text-decoration: none; +} + +.annotation-list { + margin-left: 8px; +} + +.annotation-list table { + width: 100%; + +} + +.annotation-list-range { + display: block; + font-size: 15px; + font-style: italic; + color: @theme-color-text-lighter; + vertical-align: top; + margin: 0 0 8px 8px; + padding-bottom: 20px; +} + +.empty-annotation-list, .annotation-list .loadingPiwik { + display: block; + + font-style: italic; + color: @theme-color-text-lighter; + margin: 0 0 12px 140px; +} + +.annotation-meta { + width: 159px; + text-align: left; + vertical-align: top; + font-size: 14px; + padding-top: 10px; +} + +.annotation-user { + font-style: normal; + font-size: 13px; + color: @theme-color-text-light; +} + +.annotation-user-cell { + vertical-align: top; + width: 92px; +} + +.annotation-period { + display: inline-block; + font-style: normal; + margin: 0 8px 8px 8px; + vertical-align: top; +} + +.annotation-value { + margin: 0 12px 12px 8px; + vertical-align: top; + position: relative; + font-size: 14px; +} + +.annotation-enter-edit-mode { + cursor: pointer; + font-size: 15px; +} + +.annotation-edit, .new-annotation-edit { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 98%; +} + +.annotation-star { + display: inline-block; + margin: 0 8px 8px 0; + width: 16px; +} + +.annotation-star-changeable { + cursor: pointer; +} + +.delete-annotation { + font-size: 15px; + color: #666666; + text-decoration: none; + display: NONE; + + &:hover { + color: #cc3300; + text-decoration: none; + } +} + +.annotation-manager .submit { + float: none; +} + +.edit-annotation { + font-size: 13px; + color: black; + font-style: italic; + padding: 6px 5px; + margin-top: -5px; + + &:hover { + text-decoration: none; + } +} + +.annotation-period-edit { + display: inline-block; + background: white; + color: @theme-color-text-light; + font-size: 12px; + border: 1px solid #e4e5e4; + padding: 5px 5px 6px 3px; + border-radius: 4px; + + &:hover { + background: #f1f0eb; + border-color: #a9a399; + } +} + +.annotation-period-edit>a { + text-decoration: none; + cursor: pointer; + display: block; + font-size: 14px; +} + +.annotation-period-edit>.datepicker { + position: absolute; + margin-top: 6px; + margin-left: -5px; + z-index: 15; + background: white; + border: 1px solid #e4e5e4; + border-radius: 4px; +} + +a.add-annotation { + margin: 0; +} + +td.padding { + padding: 1px 5px 6px 5px; +} + +td.wider { + width: 80px; +} diff --git a/plugins/Annotations/templates/_annotation.twig b/plugins/Annotations/templates/_annotation.twig new file mode 100644 index 0000000000000000000000000000000000000000..46f0e728eaa1f60f8cf4dde8e20d9170475f0326 --- /dev/null +++ b/plugins/Annotations/templates/_annotation.twig @@ -0,0 +1,62 @@ +<tr class="annotation" data-id="{{ annotation.id }}" data-date="{{ annotation.date }}"> + <td class="annotation-meta"> + <div class="annotation-star{% if annotation.canEditOrDelete %} annotation-star-changeable{% endif %}" data-starred="{{ annotation.starred }}" + {% if annotation.canEditOrDelete %}title="{{ 'Annotations_ClickToStarOrUnstar'|translate }}"{% endif %}> + {% if annotation.starred %} + <img src="plugins/Morpheus/images/star.png"/> + {% else %} + <img src="plugins/Morpheus/images/star_empty.png"/> + {% endif %} + </div> + + <div class="annotation-period {% if annotation.canEditOrDelete %}annotation-enter-edit-mode{% endif %}">{{ annotation.date }}</div> + {% if annotation.canEditOrDelete %} + <div class="annotation-period-edit" style="display:none;"> + <a href="#">{{ annotation.date }}</a> + <div class="datepicker" style="display:none;"/> + </div> + {% endif %} + </td> + + {% if annotation.user is defined and userLogin != 'anonymous' %} + <td class="annotation-user-cell"> + <span class="annotation-user">{{ annotation.user }}</span><br/> + + </td> + {% endif %} + + + <td class="annotation-value"> + <div class="annotation-view-mode"> + <span {% if annotation.canEditOrDelete %}title="{{ 'Annotations_ClickToEdit'|translate }}" + class="annotation-enter-edit-mode"{% endif %}>{{ annotation.note|raw }}</span> + </div> + + + {% if annotation.canEditOrDelete %} + <div class="annotation-edit-mode" style="display:none;"> + <div class="input-field"> + <input class="annotation-edit" type="text" maxlength="255" value="{{ annotation.note|raw }}"/> + </div> + <br/> + <input class="annotation-save btn" type="button" value="{{ 'General_Save'|translate }}"/> + <input class="annotation-cancel btn" type="button" value="{{ 'General_Cancel'|translate }}"/> + </div> + {% endif %} + </td> +<td class = "wider"> +{% if annotation.canEditOrDelete %} + + <span class="table-action edit-annotation annotation-enter-edit-mode" title="{{ 'Annotations_ClickToEdit'|translate }}"> + <span class="icon-edit"></span> + </span> + + <a class="delete-annotation" title="{{ 'Annotations_ClickToDelete'|translate }}"> + <span class="icon-delete"></span> + </a> + +{% endif %} +</td> + + +</tr> diff --git a/plugins/Annotations/templates/_annotationList.twig b/plugins/Annotations/templates/_annotationList.twig new file mode 100644 index 0000000000000000000000000000000000000000..8e2c570e7150f089b5e3a95afd74d54ae07fba27 --- /dev/null +++ b/plugins/Annotations/templates/_annotationList.twig @@ -0,0 +1,36 @@ +<div class="annotations"> + + {% if annotations is empty %} + <div class="empty-annotation-list">{{ 'Annotations_NoAnnotations'|translate }}</div> + {% endif %} + + <table> + + + {% for annotation in annotations %} + {% include "@Annotations/_annotation.twig" %} + {% endfor %} + + + <tr class="new-annotation-row" style="display:none;" data-date="{{ selectedDate }}"> + <td class="annotation-meta"> + <div class="annotation-star"> </div> + <div class="annotation-period-edit"> + <a href="#">{{ selectedDate }}</a> + <div class="datepicker" style="display:none;"/> + </div> + </td> + <td class="annotation-user-cell"><span class="annotation-user">{{ userLogin }}</span></td> + <td class="annotation-value"> + <div class="input-field"> + <input type="text" value="" maxlength="255" + class="new-annotation-edit" + placeholder="{{ 'Annotations_EnterAnnotationText'|translate }}"/> + </div><br/> + <input type="button" class="btn new-annotation-save" value="{{ 'General_Save'|translate }}"/> + <input type="button" class="btn new-annotation-cancel" value="{{ 'General_Cancel'|translate }}"/> + </td> + </tr> + </table> + +</div> diff --git a/plugins/Annotations/templates/getAnnotationManager.twig b/plugins/Annotations/templates/getAnnotationManager.twig new file mode 100644 index 0000000000000000000000000000000000000000..c4e9e302f49d308eb33cb171c3fdd41a69b4537b --- /dev/null +++ b/plugins/Annotations/templates/getAnnotationManager.twig @@ -0,0 +1,35 @@ +<div class="annotation-manager" + {% if startDate != endDate %}data-date="{{ startDate }},{{ endDate }}" data-period="range" + {% else %}data-date="{{ startDate }}" data-period="{{ period }}" + {% endif %}> + + <div class="annotations-header"> + <span class= "annotation">{{ 'Annotations_Annotations'|translate }}</span> + </div> + + <div class="annotation-list-range">{{ startDatePretty }}{% if startDate != endDate %} — {{ endDatePretty }}{% endif %}</div> + + <div class="annotation-list"> + {% include "@Annotations/_annotationList.twig" %} + + <span class="loadingPiwik" style="display:none;"> + {% include "@CoreHome/_loader.twig" %} + {{ 'General_Loading'|translate }} + </span> + + </div> + + <div class="annotation-controls"> + + {% if canUserAddNotes %} + <a class="add-annotation"> + <span class="icon-add"></span> + {{ 'Annotations_CreateNewAnnotation'|translate }} + </a> + {% elseif userLogin == 'anonymous' %} + <a href="index.php?module=Login">{{ 'Annotations_LoginToAnnotate'|translate }}</a> + {% endif %} + + </div> + +</div> diff --git a/plugins/Annotations/templates/getEvolutionIcons.twig b/plugins/Annotations/templates/getEvolutionIcons.twig new file mode 100644 index 0000000000000000000000000000000000000000..29f1b88e2cd44c067735420b65c4ec1862982de4 --- /dev/null +++ b/plugins/Annotations/templates/getEvolutionIcons.twig @@ -0,0 +1,14 @@ +<div class="evolution-annotations"> + {% for dateCountPair in annotationCounts %} + {% set date=dateCountPair[0] %} + {% set counts=dateCountPair[1] %} + <span data-date="{{ date }}" data-count="{{ counts.count }}" data-starred="{{ counts.starred }}" + {% if counts.count == 0 %}title="{{ 'Annotations_AddAnnotationsFor'|translate(date) }}" + {% elseif counts.count == 1 %}title="{{ 'Annotations_AnnotationOnDate'|translate(date, + (counts.note|e('html_attr'))) }} +{{ 'Annotations_ClickToEditOrAdd'|translate }}" + {% else %}title="{{ 'Annotations_ViewAndAddAnnotations'|translate(date) }}"{% endif %}> + <span class="icon-annotation {% if counts.starred > 0 %}starred{% endif %}"></span> + </span> + {% endfor %} +</div> diff --git a/plugins/Annotations/templates/saveAnnotation.twig b/plugins/Annotations/templates/saveAnnotation.twig new file mode 100644 index 0000000000000000000000000000000000000000..e5cd120205bb68479a83475e73cf6853e75d45c4 --- /dev/null +++ b/plugins/Annotations/templates/saveAnnotation.twig @@ -0,0 +1 @@ +{% include "@Annotations/_annotation.twig" %} \ No newline at end of file diff --git a/plugins/Annotations/tests/System/AnnotationsTest.php b/plugins/Annotations/tests/System/AnnotationsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2ed56382660c1e5e51a580b036b8bed8f4766546 --- /dev/null +++ b/plugins/Annotations/tests/System/AnnotationsTest.php @@ -0,0 +1,410 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Annotations\tests\System; + +use Piwik\API\Request; +use Piwik\Date; +use Piwik\Plugins\Annotations\Annotations; +use Piwik\Plugins\Annotations\API; +use Piwik\Tests\Framework\Mock\FakeAccess; +use Piwik\Tests\Framework\TestCase\SystemTestCase; +use Piwik\Tests\Fixtures\TwoSitesWithAnnotations; +use Exception; + +/** + * @group Plugins + * @group AnnotationsTest + */ +class AnnotationsTest extends SystemTestCase +{ + /** @var TwoSitesWithAnnotations */ + public static $fixture = null; + + public function setUp(): void + { + parent::setUp(); + + // Fixed time necessary for "last30" API tests. + Date::$now = strtotime('2012-03-03 12:00:00'); + } + + public function tearDown(): void + { + Date::$now = null; + + parent::tearDown(); + } + + public static function getOutputPrefix() + { + return 'annotations'; + } + + public function getApiForTesting() + { + $idSite1 = self::$fixture->idSite1; + + return array( + + // get + array('Annotations.get', array('idSite' => $idSite1, + 'date' => '2012-01-01', + 'periods' => 'day', + 'otherRequestParameters' => array('idNote' => 1))), + + // getAll + array('Annotations.getAll', array('idSite' => $idSite1, + 'date' => '2011-12-01', + 'periods' => array('day', 'week', 'month'))), + array('Annotations.getAll', array('idSite' => $idSite1, + 'date' => '2012-01-01', + 'periods' => array('year'))), + array('Annotations.getAll', array('idSite' => $idSite1, + 'date' => '2012-03-01', + 'periods' => array('week'), + 'otherRequestParameters' => array('lastN' => 6), + 'testSuffix' => '_lastN')), + array('Annotations.getAll', array('idSite' => $idSite1, + 'date' => '2012-01-26,2012-03-01', + 'periods' => array('week'), + 'testSuffix' => '_multiplePeriod')), + array('Annotations.getAll', array('idSite' => $idSite1, + 'date' => '2012-01-15,2012-02-15', + 'periods' => array('range'), + 'otherRequestParameters' => array('lastN' => 6), + 'testSuffix' => '_range')), + array('Annotations.getAll', array('idSite' => $idSite1, + 'date' => 'last30', + 'periods' => array('range'), + 'otherRequestParameters' => array('lastN' => 6), + 'testSuffix' => '_last30')), + array('Annotations.getAll', array('idSite' => 'all', + 'date' => '2012-01-01', + 'periods' => array('month'), + 'testSuffix' => '_multipleSites')), + array('Annotations.getAll', array('idSite' => $idSite1, + 'testSuffix' => '_noDate')), + + // getAnnotationCountForDates + array('Annotations.getAnnotationCountForDates', array('idSite' => $idSite1, + 'date' => '2011-12-01', + 'periods' => array('day', 'week', 'month'))), + array('Annotations.getAnnotationCountForDates', array('idSite' => $idSite1, + 'date' => '2012-01-01', + 'periods' => array('year'))), + array('Annotations.getAnnotationCountForDates', array('idSite' => $idSite1, + 'date' => '2012-03-01', + 'periods' => array('week'), + 'otherRequestParameters' => array('lastN' => 6), + 'testSuffix' => '_lastN')), + array('Annotations.getAnnotationCountForDates', array('idSite' => $idSite1, + 'date' => '2012-01-15,2012-02-15', + 'periods' => array('range'), + 'otherRequestParameters' => array('lastN' => 6), + 'testSuffix' => '_range')), + array('Annotations.getAnnotationCountForDates', array('idSite' => $idSite1, + 'date' => 'last30', + 'periods' => array('range'), + 'otherRequestParameters' => array('lastN' => 6), + 'testSuffix' => '_last30')), + array('Annotations.getAnnotationCountForDates', array('idSite' => 'all', + 'date' => '2012-01-01', + 'periods' => array('month'), + 'testSuffix' => '_multipleSites')), + ); + } + + /** + * @dataProvider getApiForTesting + */ + public function testApi($api, $params) + { + $this->runApiTests($api, $params); + } + + /** + * @dataProvider getDateRangeForPeriodEndDateLimitData + */ + public function testGetDateRangeForPeriodLimitsUnrestrictedEndDate( + string $date, + string $period, + string $expectedStartDate, + string $expectedEndDate + ) { + // Date::$now is fixed to 2012-03-03 in setUp(), so the max allowed end date is end of 2022. + [$startDate, $endDate] = Annotations::getDateRangeForPeriod($date, $period); + + $this->assertSame($expectedStartDate, $startDate->toString()); + $this->assertSame($expectedEndDate, $endDate->toString()); + } + + public function getDateRangeForPeriodEndDateLimitData(): iterable + { + // an unrestricted date range far in the future is clamped to the max allowed end date + yield 'unrestricted future range' => ['1992-01-30,9999-04-30', 'day', '1992-01-30', '2022-12-31']; + + // a regular multiple period range within the allowed window is left untouched + yield 'regular range' => ['2012-01-26,2012-03-01', 'week', '2012-01-26', '2012-03-01']; + } + + /** + * @dataProvider getTestAddSuccessData + */ + public function testAddSuccess(string $date, string $expectedDate) + { + $addResponse = API::getInstance()->add( + self::$fixture->idSite1, + $date, + $note = 'new note text single add' + ); + unset($addResponse['id']); // unsetting id for comparison + unset($addResponse['idNote']); + + $expectedAddResponse = [ + 'date' => $expectedDate, + 'note' => 'new note text single add', + 'starred' => 0, + 'user' => 'superUserLogin', + 'idsite' => 1, + 'canEditOrDelete' => true, + ]; + + $this->assertEquals($expectedAddResponse, $addResponse); + } + + public function getTestAddSuccessData(): iterable + { + yield 'specific date' => ['2012-04-01', '2012-04-01']; + yield 'today' => ['today', '2012-03-03']; // based on fixed date in the test + yield 'yesterday' => ['yesterday', '2012-03-02']; + yield 'tomorrow' => ['tomorrow', '2012-03-04']; + } + + public function testAddMultipleSitesFail() + { + self::expectError(); + + API::getInstance()->add("1,2,3", "2012-01-01", "whatever"); + } + + public function testAddInvalidDateFail() + { + self::expectException(Exception::class); + + API::getInstance()->add(self::$fixture->idSite1, "invaliddate", "whatever"); + } + + public function testSaveMultipleSitesFail() + { + self::expectError(); + + API::getInstance()->save("1,2,3", 0); + } + + public function testSaveInvalidDateFail() + { + self::expectException(Exception::class); + + API::getInstance()->save(self::$fixture->idSite1, 0, "invaliddate"); + } + + public function testSaveInvalidNoteIdFail() + { + self::expectException(Exception::class); + + API::getInstance()->save(self::$fixture->idSite1, -1); + } + + public function testDeleteMultipleSitesFail() + { + self::expectError(); + + API::getInstance()->delete("1,2,3", 0); + } + + public function testDeleteInvalidNoteIdFail() + { + self::expectException(Exception::class); + + API::getInstance()->delete(self::$fixture->idSite1, -1); + } + + public function testGetMultipleSitesFail() + { + self::expectError(); + + API::getInstance()->get("1,2,3", 0); + } + + public function testGetInvalidNoteIdFail() + { + self::expectException(Exception::class); + + API::getInstance()->get(self::$fixture->idSite1, -1); + } + + public function testSaveSuccess() + { + API::getInstance()->save( + self::$fixture->idSite1, + 1, + $date = '2011-04-01', + $note = 'new note text', + $starred = 1 + ); + + $expectedAnnotation = array( + 'date' => '2011-04-01', + 'note' => 'new note text', + 'starred' => 1, + 'user' => 'superUserLogin', + 'id' => 1, + 'idsite' => 1, + 'canEditOrDelete' => true, + 'idNote' => 1, + ); + $this->assertEquals($expectedAnnotation, API::getInstance()->get(self::$fixture->idSite1, 1)); + } + + public function testSaveSuccessWithoutDate() + { + API::getInstance()->save( + self::$fixture->idSite1, + 1, + $date = null, + $note = 'new note text', + $starred = 1 + ); + + $expectedAnnotation = array( + 'date' => '2011-04-01', + 'note' => 'new note text', + 'starred' => 1, + 'user' => 'superUserLogin', + 'id' => 1, + 'idsite' => 1, + 'canEditOrDelete' => true, + 'idNote' => 1, + ); + $this->assertEquals($expectedAnnotation, API::getInstance()->get(self::$fixture->idSite1, 1)); + } + + public function testSaveSuccessChangesDateToTomorrow() + { + API::getInstance()->save( + self::$fixture->idSite1, + 1, + $date = 'tomorrow', + $note = 'updated note text for tomorrow, not starred', + $starred = 0 + ); + + $expectedAnnotation = array( + 'date' => '2012-03-04', + 'note' => 'updated note text for tomorrow, not starred', + 'starred' => 0, + 'user' => 'superUserLogin', + 'id' => 1, + 'idsite' => 1, + 'canEditOrDelete' => true, + 'idNote' => 1, + ); + $this->assertEquals($expectedAnnotation, API::getInstance()->get(self::$fixture->idSite1, 1)); + } + + public function testSaveNoChangesSuccess() + { + API::getInstance()->save(self::$fixture->idSite1, 3); + + $expectedAnnotation = array( + 'date' => '2011-12-02', + 'note' => '1: Site 1 annotation for 2011-12-02', + 'starred' => 0, + 'user' => 'superUserLogin', + 'id' => 3, + 'idsite' => 1, + 'canEditOrDelete' => true, + 'idNote' => 3, + ); + $this->assertEquals($expectedAnnotation, API::getInstance()->get(self::$fixture->idSite1, 3)); + } + + public function testDeleteSuccess() + { + self::expectException(Exception::class); + + API::getInstance()->delete(self::$fixture->idSite1, 1); + API::getInstance()->get(self::$fixture->idSite1, 1); + } + + public function getPermissionsFailData() + { + return array( + // getAll + array(false, false, "module=API&method=Annotations.getAll&idSite=1&date=2012-01-01&period=year", true, "getAll should throw if user does not have view access"), + + // get + array(false, false, "module=API&method=Annotations.get&idSite=1&idNote=1", true, "get should throw if user does not have view access"), + + // getAnnotationCountForDates + array(false, false, "module=API&method=Annotations.getAnnotationCountForDates&idSite=1&date=2012-01-01&period=year", true, "getAnnotationCountForDates should throw if user does not have view access"), + + // add + array(false, false, "module=API&method=Annotations.add&idSite=1&date=2011-02-01¬e=whatever", true, "add should throw if user does not have view access"), + array(false, true, "module=API&method=Annotations.add&idSite=1&date=2011-02-01¬e=whatever2", false, "add should not throw if user has view access"), + array(true, true, "module=API&method=Annotations.add&idSite=1&date=2011-02-01¬e=whatever3", false, "add should not throw if user has admin access"), + + // save + array(false, false, "module=API&method=Annotations.save&idSite=1&idNote=1&date=2011-03-01¬e=newnote", true, "save should throw if user does not have view access"), + array(false, true, "module=API&method=Annotations.save&idSite=1&idNote=1&date=2011-03-01¬e=newnote", true, "save should throw if user has view access but did not edit note"), + array(true, true, "module=API&method=Annotations.save&idSite=1&idNote=53&date=2011-03-01¬e=newnote", false, "save should not throw if user has admin access"), + + // delete + array(false, false, "module=API&method=Annotations.delete&idSite=1&idNote=1", true, "delete should throw if user does not have view access"), + array(false, true, "module=API&method=Annotations.delete&idSite=1&idNote=1", true, "delete should throw if user does not have view access"), + array(true, true, "module=API&method=Annotations.delete&idSite=1&idNote=53", false, "delete should not throw if user has admin access"), + ); + } + + /** + * @dataProvider getPermissionsFailData + */ + public function testMethodPermissions($hasAdminAccess, $hasViewAccess, $request, $checkException, $failMessage) + { + if (true === $checkException) { + self::expectException(Exception::class); + } else { + self::expectNotToPerformAssertions(); + } + + // create fake access that denies user access + FakeAccess::clearAccess(false); + FakeAccess::$identity = 'user' . (int)$hasAdminAccess . (int)$hasViewAccess; + FakeAccess::$idSitesAdmin = $hasAdminAccess ? array(self::$fixture->idSite1) : array(); + FakeAccess::$idSitesView = $hasViewAccess ? array(self::$fixture->idSite1) : array(); + + $request = new Request(Request::getRequestArrayFromString($request . '&format=original')); + $request->process(); + } + + public static function getPathToTestDirectory() + { + return dirname(__FILE__); + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess(), + ); + } +} + +AnnotationsTest::$fixture = new TwoSitesWithAnnotations(); diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.get.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.get.xml new file mode 100644 index 0000000000000000000000000000000000000000..cfcb4c37218a8d7b6be08705773fda677dc17acf --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.get.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <id>1</id> + <idsite>1</idsite> + <date>2011-12-01</date> + <note>0: Site 1 annotation for 2011-12-01</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>1</idNote> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_day.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..b3ca9d639207198f3909f9fc475d5d6cb0101bcd --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_day.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>1</id> + <idsite>1</idsite> + <date>2011-12-01</date> + <note>0: Site 1 annotation for 2011-12-01</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>1</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_month.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..0fe6a7b1439711917f3cd14cb8d04a7866287687 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_month.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>1</id> + <idsite>1</idsite> + <date>2011-12-01</date> + <note>0: Site 1 annotation for 2011-12-01</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>1</idNote> + </row> + <row> + <id>3</id> + <idsite>1</idsite> + <date>2011-12-02</date> + <note>1: Site 1 annotation for 2011-12-02</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>3</idNote> + </row> + <row> + <id>5</id> + <idsite>1</idsite> + <date>2011-12-08</date> + <note>2: Site 1 annotation for 2011-12-08</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>5</idNote> + </row> + <row> + <id>7</id> + <idsite>1</idsite> + <date>2011-12-09</date> + <note>3: Site 1 annotation for 2011-12-09</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>7</idNote> + </row> + <row> + <id>9</id> + <idsite>1</idsite> + <date>2011-12-15</date> + <note>4: Site 1 annotation for 2011-12-15</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>9</idNote> + </row> + <row> + <id>11</id> + <idsite>1</idsite> + <date>2011-12-16</date> + <note>5: Site 1 annotation for 2011-12-16</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>11</idNote> + </row> + <row> + <id>13</id> + <idsite>1</idsite> + <date>2011-12-22</date> + <note>6: Site 1 annotation for 2011-12-22</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>13</idNote> + </row> + <row> + <id>15</id> + <idsite>1</idsite> + <date>2011-12-23</date> + <note>7: Site 1 annotation for 2011-12-23</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>15</idNote> + </row> + <row> + <id>17</id> + <idsite>1</idsite> + <date>2011-12-29</date> + <note>8: Site 1 annotation for 2011-12-29</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>17</idNote> + </row> + <row> + <id>19</id> + <idsite>1</idsite> + <date>2011-12-30</date> + <note>9: Site 1 annotation for 2011-12-30</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>19</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_week.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..4c10a1e683e2c22a32e15d7a7b281bc6fc23732b --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_week.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>1</id> + <idsite>1</idsite> + <date>2011-12-01</date> + <note>0: Site 1 annotation for 2011-12-01</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>1</idNote> + </row> + <row> + <id>3</id> + <idsite>1</idsite> + <date>2011-12-02</date> + <note>1: Site 1 annotation for 2011-12-02</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>3</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_year.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..3bcf3f37582259e59ab724fa95bce133b2b28df1 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAll_year.xml @@ -0,0 +1,215 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>21</id> + <idsite>1</idsite> + <date>2012-01-05</date> + <note>10: Site 1 annotation for 2012-01-05</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>21</idNote> + </row> + <row> + <id>23</id> + <idsite>1</idsite> + <date>2012-01-06</date> + <note>11: Site 1 annotation for 2012-01-06</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>23</idNote> + </row> + <row> + <id>25</id> + <idsite>1</idsite> + <date>2012-01-12</date> + <note>12: Site 1 annotation for 2012-01-12</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>25</idNote> + </row> + <row> + <id>27</id> + <idsite>1</idsite> + <date>2012-01-13</date> + <note>13: Site 1 annotation for 2012-01-13</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>27</idNote> + </row> + <row> + <id>29</id> + <idsite>1</idsite> + <date>2012-01-19</date> + <note>14: Site 1 annotation for 2012-01-19</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>29</idNote> + </row> + <row> + <id>31</id> + <idsite>1</idsite> + <date>2012-01-20</date> + <note>15: Site 1 annotation for 2012-01-20</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>31</idNote> + </row> + <row> + <id>33</id> + <idsite>1</idsite> + <date>2012-01-26</date> + <note>16: Site 1 annotation for 2012-01-26</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>33</idNote> + </row> + <row> + <id>35</id> + <idsite>1</idsite> + <date>2012-01-27</date> + <note>17: Site 1 annotation for 2012-01-27</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>35</idNote> + </row> + <row> + <id>37</id> + <idsite>1</idsite> + <date>2012-02-02</date> + <note>18: Site 1 annotation for 2012-02-02</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>37</idNote> + </row> + <row> + <id>39</id> + <idsite>1</idsite> + <date>2012-02-03</date> + <note>19: Site 1 annotation for 2012-02-03</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>39</idNote> + </row> + <row> + <id>41</id> + <idsite>1</idsite> + <date>2012-02-09</date> + <note>20: Site 1 annotation for 2012-02-09</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>41</idNote> + </row> + <row> + <id>43</id> + <idsite>1</idsite> + <date>2012-02-10</date> + <note>21: Site 1 annotation for 2012-02-10</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>43</idNote> + </row> + <row> + <id>45</id> + <idsite>1</idsite> + <date>2012-02-16</date> + <note>22: Site 1 annotation for 2012-02-16</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>45</idNote> + </row> + <row> + <id>47</id> + <idsite>1</idsite> + <date>2012-02-17</date> + <note>23: Site 1 annotation for 2012-02-17</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>47</idNote> + </row> + <row> + <id>49</id> + <idsite>1</idsite> + <date>2012-02-23</date> + <note>24: Site 1 annotation for 2012-02-23</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>49</idNote> + </row> + <row> + <id>51</id> + <idsite>1</idsite> + <date>2012-02-24</date> + <note>25: Site 1 annotation for 2012-02-24</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>51</idNote> + </row> + <row> + <id>53</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note><h1>Test</h1></note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>53</idNote> + </row> + <row> + <id>54</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note><script>alert(1)</script></note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>54</idNote> + </row> + <row> + <id>55</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note>{{ 1+1 }}</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>55</idNote> + </row> + <row> + <id>56</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note><div vue-entry="Dashboard.DashboardSettings"></div></note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>56</idNote> + </row> + <row> + <id>57</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note>1' or '1' = '1'))/*</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>57</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_day.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..7409bdf738b0f438d4d60a11acbc90c639bb81d7 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_day.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <row>2011-12-01</row> + <row> + <count>1</count> + <starred>1</starred> + </row> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_month.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..152be4e2725342c7942008b04b46856dd71dcd23 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_month.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <row>2011-12-01</row> + <row> + <count>10</count> + <starred>4</starred> + </row> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_week.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..d428059285740985039c34372216e6525861dab4 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_week.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <row>2011-11-28</row> + <row> + <count>2</count> + <starred>1</starred> + </row> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_year.xml b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..92b4e05247258561fd12e41765537da9f9b73d1d --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations__Annotations.getAnnotationCountForDates_year.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <row>2012-01-01</row> + <row> + <count>21</count> + <starred>5</starred> + </row> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_last30__Annotations.getAll_range.xml b/plugins/Annotations/tests/System/expected/test_annotations_last30__Annotations.getAll_range.xml new file mode 100644 index 0000000000000000000000000000000000000000..c8def10512e5ead2e60ff1c4b752e52ad59a9a52 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_last30__Annotations.getAll_range.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>39</id> + <idsite>1</idsite> + <date>2012-02-03</date> + <note>19: Site 1 annotation for 2012-02-03</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>39</idNote> + </row> + <row> + <id>41</id> + <idsite>1</idsite> + <date>2012-02-09</date> + <note>20: Site 1 annotation for 2012-02-09</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>41</idNote> + </row> + <row> + <id>43</id> + <idsite>1</idsite> + <date>2012-02-10</date> + <note>21: Site 1 annotation for 2012-02-10</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>43</idNote> + </row> + <row> + <id>45</id> + <idsite>1</idsite> + <date>2012-02-16</date> + <note>22: Site 1 annotation for 2012-02-16</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>45</idNote> + </row> + <row> + <id>47</id> + <idsite>1</idsite> + <date>2012-02-17</date> + <note>23: Site 1 annotation for 2012-02-17</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>47</idNote> + </row> + <row> + <id>49</id> + <idsite>1</idsite> + <date>2012-02-23</date> + <note>24: Site 1 annotation for 2012-02-23</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>49</idNote> + </row> + <row> + <id>51</id> + <idsite>1</idsite> + <date>2012-02-24</date> + <note>25: Site 1 annotation for 2012-02-24</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>51</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_last30__Annotations.getAnnotationCountForDates_range.xml b/plugins/Annotations/tests/System/expected/test_annotations_last30__Annotations.getAnnotationCountForDates_range.xml new file mode 100644 index 0000000000000000000000000000000000000000..d46c8868e538e5a75a90fc57707d603cfc109f72 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_last30__Annotations.getAnnotationCountForDates_range.xml @@ -0,0 +1,215 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <row>2012-02-03</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-04</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-05</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-06</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-07</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-08</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-09</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-10</row> + <row> + <count>1</count> + <starred>1</starred> + </row> + </row> + <row> + <row>2012-02-11</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-12</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-13</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-14</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-15</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-16</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-17</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-18</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-19</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-20</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-21</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-22</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-23</row> + <row> + <count>1</count> + <starred>1</starred> + </row> + </row> + <row> + <row>2012-02-24</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-25</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-26</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-27</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-28</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-29</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-03-01</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-03-02</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-03-03</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_lastN__Annotations.getAll_week.xml b/plugins/Annotations/tests/System/expected/test_annotations_lastN__Annotations.getAll_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..69e1c7df8bf4c5ea91a8053fc11327c65f914096 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_lastN__Annotations.getAll_week.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>33</id> + <idsite>1</idsite> + <date>2012-01-26</date> + <note>16: Site 1 annotation for 2012-01-26</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>33</idNote> + </row> + <row> + <id>35</id> + <idsite>1</idsite> + <date>2012-01-27</date> + <note>17: Site 1 annotation for 2012-01-27</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>35</idNote> + </row> + <row> + <id>37</id> + <idsite>1</idsite> + <date>2012-02-02</date> + <note>18: Site 1 annotation for 2012-02-02</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>37</idNote> + </row> + <row> + <id>39</id> + <idsite>1</idsite> + <date>2012-02-03</date> + <note>19: Site 1 annotation for 2012-02-03</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>39</idNote> + </row> + <row> + <id>41</id> + <idsite>1</idsite> + <date>2012-02-09</date> + <note>20: Site 1 annotation for 2012-02-09</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>41</idNote> + </row> + <row> + <id>43</id> + <idsite>1</idsite> + <date>2012-02-10</date> + <note>21: Site 1 annotation for 2012-02-10</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>43</idNote> + </row> + <row> + <id>45</id> + <idsite>1</idsite> + <date>2012-02-16</date> + <note>22: Site 1 annotation for 2012-02-16</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>45</idNote> + </row> + <row> + <id>47</id> + <idsite>1</idsite> + <date>2012-02-17</date> + <note>23: Site 1 annotation for 2012-02-17</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>47</idNote> + </row> + <row> + <id>49</id> + <idsite>1</idsite> + <date>2012-02-23</date> + <note>24: Site 1 annotation for 2012-02-23</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>49</idNote> + </row> + <row> + <id>51</id> + <idsite>1</idsite> + <date>2012-02-24</date> + <note>25: Site 1 annotation for 2012-02-24</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>51</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_lastN__Annotations.getAnnotationCountForDates_week.xml b/plugins/Annotations/tests/System/expected/test_annotations_lastN__Annotations.getAnnotationCountForDates_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..2abbab5b3f3f683aaf0cc5dab190e2cbff95bddf --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_lastN__Annotations.getAnnotationCountForDates_week.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <row>2012-01-23</row> + <row> + <count>2</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-30</row> + <row> + <count>2</count> + <starred>1</starred> + </row> + </row> + <row> + <row>2012-02-06</row> + <row> + <count>2</count> + <starred>1</starred> + </row> + </row> + <row> + <row>2012-02-13</row> + <row> + <count>2</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-20</row> + <row> + <count>2</count> + <starred>1</starred> + </row> + </row> + <row> + <row>2012-02-27</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_multiplePeriod__Annotations.getAll_week.xml b/plugins/Annotations/tests/System/expected/test_annotations_multiplePeriod__Annotations.getAll_week.xml new file mode 100644 index 0000000000000000000000000000000000000000..69e1c7df8bf4c5ea91a8053fc11327c65f914096 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_multiplePeriod__Annotations.getAll_week.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>33</id> + <idsite>1</idsite> + <date>2012-01-26</date> + <note>16: Site 1 annotation for 2012-01-26</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>33</idNote> + </row> + <row> + <id>35</id> + <idsite>1</idsite> + <date>2012-01-27</date> + <note>17: Site 1 annotation for 2012-01-27</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>35</idNote> + </row> + <row> + <id>37</id> + <idsite>1</idsite> + <date>2012-02-02</date> + <note>18: Site 1 annotation for 2012-02-02</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>37</idNote> + </row> + <row> + <id>39</id> + <idsite>1</idsite> + <date>2012-02-03</date> + <note>19: Site 1 annotation for 2012-02-03</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>39</idNote> + </row> + <row> + <id>41</id> + <idsite>1</idsite> + <date>2012-02-09</date> + <note>20: Site 1 annotation for 2012-02-09</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>41</idNote> + </row> + <row> + <id>43</id> + <idsite>1</idsite> + <date>2012-02-10</date> + <note>21: Site 1 annotation for 2012-02-10</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>43</idNote> + </row> + <row> + <id>45</id> + <idsite>1</idsite> + <date>2012-02-16</date> + <note>22: Site 1 annotation for 2012-02-16</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>45</idNote> + </row> + <row> + <id>47</id> + <idsite>1</idsite> + <date>2012-02-17</date> + <note>23: Site 1 annotation for 2012-02-17</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>47</idNote> + </row> + <row> + <id>49</id> + <idsite>1</idsite> + <date>2012-02-23</date> + <note>24: Site 1 annotation for 2012-02-23</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>49</idNote> + </row> + <row> + <id>51</id> + <idsite>1</idsite> + <date>2012-02-24</date> + <note>25: Site 1 annotation for 2012-02-24</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>51</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_multipleSites__Annotations.getAll_month.xml b/plugins/Annotations/tests/System/expected/test_annotations_multipleSites__Annotations.getAll_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..54df7b4037f61d8c49635fd469828486c04be631 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_multipleSites__Annotations.getAll_month.xml @@ -0,0 +1,167 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>21</id> + <idsite>1</idsite> + <date>2012-01-05</date> + <note>10: Site 1 annotation for 2012-01-05</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>21</idNote> + </row> + <row> + <id>23</id> + <idsite>1</idsite> + <date>2012-01-06</date> + <note>11: Site 1 annotation for 2012-01-06</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>23</idNote> + </row> + <row> + <id>25</id> + <idsite>1</idsite> + <date>2012-01-12</date> + <note>12: Site 1 annotation for 2012-01-12</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>25</idNote> + </row> + <row> + <id>27</id> + <idsite>1</idsite> + <date>2012-01-13</date> + <note>13: Site 1 annotation for 2012-01-13</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>27</idNote> + </row> + <row> + <id>29</id> + <idsite>1</idsite> + <date>2012-01-19</date> + <note>14: Site 1 annotation for 2012-01-19</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>29</idNote> + </row> + <row> + <id>31</id> + <idsite>1</idsite> + <date>2012-01-20</date> + <note>15: Site 1 annotation for 2012-01-20</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>31</idNote> + </row> + <row> + <id>33</id> + <idsite>1</idsite> + <date>2012-01-26</date> + <note>16: Site 1 annotation for 2012-01-26</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>33</idNote> + </row> + <row> + <id>35</id> + <idsite>1</idsite> + <date>2012-01-27</date> + <note>17: Site 1 annotation for 2012-01-27</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>35</idNote> + </row> + </row> + <row key="2"> + <row> + <id>22</id> + <idsite>2</idsite> + <date>2012-01-05</date> + <note>10: Site 2 annotation for 2012-01-05</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>22</idNote> + </row> + <row> + <id>24</id> + <idsite>2</idsite> + <date>2012-01-06</date> + <note>11: Site 2 annotation for 2012-01-06</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>24</idNote> + </row> + <row> + <id>26</id> + <idsite>2</idsite> + <date>2012-01-12</date> + <note>12: Site 2 annotation for 2012-01-12</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>26</idNote> + </row> + <row> + <id>28</id> + <idsite>2</idsite> + <date>2012-01-13</date> + <note>13: Site 2 annotation for 2012-01-13</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>28</idNote> + </row> + <row> + <id>30</id> + <idsite>2</idsite> + <date>2012-01-19</date> + <note>14: Site 2 annotation for 2012-01-19</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>30</idNote> + </row> + <row> + <id>32</id> + <idsite>2</idsite> + <date>2012-01-20</date> + <note>15: Site 2 annotation for 2012-01-20</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>32</idNote> + </row> + <row> + <id>34</id> + <idsite>2</idsite> + <date>2012-01-26</date> + <note>16: Site 2 annotation for 2012-01-26</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>34</idNote> + </row> + <row> + <id>36</id> + <idsite>2</idsite> + <date>2012-01-27</date> + <note>17: Site 2 annotation for 2012-01-27</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>36</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_multipleSites__Annotations.getAnnotationCountForDates_month.xml b/plugins/Annotations/tests/System/expected/test_annotations_multipleSites__Annotations.getAnnotationCountForDates_month.xml new file mode 100644 index 0000000000000000000000000000000000000000..8549b62364f02f55891e9ad3568b24ad0097b780 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_multipleSites__Annotations.getAnnotationCountForDates_month.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <row>2012-01-01</row> + <row> + <count>8</count> + <starred>2</starred> + </row> + </row> + </row> + <row key="2"> + <row> + <row>2012-01-01</row> + <row> + <count>8</count> + <starred>2</starred> + </row> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_noDate__Annotations.getAll_day.xml b/plugins/Annotations/tests/System/expected/test_annotations_noDate__Annotations.getAll_day.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ffb417ee18416c7b78b670ca4e7c8b21ca4d7b4 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_noDate__Annotations.getAll_day.xml @@ -0,0 +1,315 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>1</id> + <idsite>1</idsite> + <date>2011-12-01</date> + <note>0: Site 1 annotation for 2011-12-01</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>1</idNote> + </row> + <row> + <id>3</id> + <idsite>1</idsite> + <date>2011-12-02</date> + <note>1: Site 1 annotation for 2011-12-02</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>3</idNote> + </row> + <row> + <id>5</id> + <idsite>1</idsite> + <date>2011-12-08</date> + <note>2: Site 1 annotation for 2011-12-08</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>5</idNote> + </row> + <row> + <id>7</id> + <idsite>1</idsite> + <date>2011-12-09</date> + <note>3: Site 1 annotation for 2011-12-09</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>7</idNote> + </row> + <row> + <id>9</id> + <idsite>1</idsite> + <date>2011-12-15</date> + <note>4: Site 1 annotation for 2011-12-15</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>9</idNote> + </row> + <row> + <id>11</id> + <idsite>1</idsite> + <date>2011-12-16</date> + <note>5: Site 1 annotation for 2011-12-16</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>11</idNote> + </row> + <row> + <id>13</id> + <idsite>1</idsite> + <date>2011-12-22</date> + <note>6: Site 1 annotation for 2011-12-22</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>13</idNote> + </row> + <row> + <id>15</id> + <idsite>1</idsite> + <date>2011-12-23</date> + <note>7: Site 1 annotation for 2011-12-23</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>15</idNote> + </row> + <row> + <id>17</id> + <idsite>1</idsite> + <date>2011-12-29</date> + <note>8: Site 1 annotation for 2011-12-29</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>17</idNote> + </row> + <row> + <id>19</id> + <idsite>1</idsite> + <date>2011-12-30</date> + <note>9: Site 1 annotation for 2011-12-30</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>19</idNote> + </row> + <row> + <id>21</id> + <idsite>1</idsite> + <date>2012-01-05</date> + <note>10: Site 1 annotation for 2012-01-05</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>21</idNote> + </row> + <row> + <id>23</id> + <idsite>1</idsite> + <date>2012-01-06</date> + <note>11: Site 1 annotation for 2012-01-06</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>23</idNote> + </row> + <row> + <id>25</id> + <idsite>1</idsite> + <date>2012-01-12</date> + <note>12: Site 1 annotation for 2012-01-12</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>25</idNote> + </row> + <row> + <id>27</id> + <idsite>1</idsite> + <date>2012-01-13</date> + <note>13: Site 1 annotation for 2012-01-13</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>27</idNote> + </row> + <row> + <id>29</id> + <idsite>1</idsite> + <date>2012-01-19</date> + <note>14: Site 1 annotation for 2012-01-19</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>29</idNote> + </row> + <row> + <id>31</id> + <idsite>1</idsite> + <date>2012-01-20</date> + <note>15: Site 1 annotation for 2012-01-20</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>31</idNote> + </row> + <row> + <id>33</id> + <idsite>1</idsite> + <date>2012-01-26</date> + <note>16: Site 1 annotation for 2012-01-26</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>33</idNote> + </row> + <row> + <id>35</id> + <idsite>1</idsite> + <date>2012-01-27</date> + <note>17: Site 1 annotation for 2012-01-27</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>35</idNote> + </row> + <row> + <id>37</id> + <idsite>1</idsite> + <date>2012-02-02</date> + <note>18: Site 1 annotation for 2012-02-02</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>37</idNote> + </row> + <row> + <id>39</id> + <idsite>1</idsite> + <date>2012-02-03</date> + <note>19: Site 1 annotation for 2012-02-03</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>39</idNote> + </row> + <row> + <id>41</id> + <idsite>1</idsite> + <date>2012-02-09</date> + <note>20: Site 1 annotation for 2012-02-09</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>41</idNote> + </row> + <row> + <id>43</id> + <idsite>1</idsite> + <date>2012-02-10</date> + <note>21: Site 1 annotation for 2012-02-10</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>43</idNote> + </row> + <row> + <id>45</id> + <idsite>1</idsite> + <date>2012-02-16</date> + <note>22: Site 1 annotation for 2012-02-16</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>45</idNote> + </row> + <row> + <id>47</id> + <idsite>1</idsite> + <date>2012-02-17</date> + <note>23: Site 1 annotation for 2012-02-17</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>47</idNote> + </row> + <row> + <id>49</id> + <idsite>1</idsite> + <date>2012-02-23</date> + <note>24: Site 1 annotation for 2012-02-23</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>49</idNote> + </row> + <row> + <id>51</id> + <idsite>1</idsite> + <date>2012-02-24</date> + <note>25: Site 1 annotation for 2012-02-24</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>51</idNote> + </row> + <row> + <id>53</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note><h1>Test</h1></note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>53</idNote> + </row> + <row> + <id>54</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note><script>alert(1)</script></note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>54</idNote> + </row> + <row> + <id>55</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note>{{ 1+1 }}</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>55</idNote> + </row> + <row> + <id>56</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note><div vue-entry="Dashboard.DashboardSettings"></div></note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>56</idNote> + </row> + <row> + <id>57</id> + <idsite>1</idsite> + <date>2012-04-01</date> + <note>1' or '1' = '1'))/*</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>57</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_range__Annotations.getAll_range.xml b/plugins/Annotations/tests/System/expected/test_annotations_range__Annotations.getAll_range.xml new file mode 100644 index 0000000000000000000000000000000000000000..401be76f5eaa5844a07fff4b3a715b98489e2e71 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_range__Annotations.getAll_range.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <id>29</id> + <idsite>1</idsite> + <date>2012-01-19</date> + <note>14: Site 1 annotation for 2012-01-19</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>29</idNote> + </row> + <row> + <id>31</id> + <idsite>1</idsite> + <date>2012-01-20</date> + <note>15: Site 1 annotation for 2012-01-20</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>31</idNote> + </row> + <row> + <id>33</id> + <idsite>1</idsite> + <date>2012-01-26</date> + <note>16: Site 1 annotation for 2012-01-26</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>33</idNote> + </row> + <row> + <id>35</id> + <idsite>1</idsite> + <date>2012-01-27</date> + <note>17: Site 1 annotation for 2012-01-27</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>35</idNote> + </row> + <row> + <id>37</id> + <idsite>1</idsite> + <date>2012-02-02</date> + <note>18: Site 1 annotation for 2012-02-02</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>37</idNote> + </row> + <row> + <id>39</id> + <idsite>1</idsite> + <date>2012-02-03</date> + <note>19: Site 1 annotation for 2012-02-03</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>39</idNote> + </row> + <row> + <id>41</id> + <idsite>1</idsite> + <date>2012-02-09</date> + <note>20: Site 1 annotation for 2012-02-09</note> + <starred>0</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>41</idNote> + </row> + <row> + <id>43</id> + <idsite>1</idsite> + <date>2012-02-10</date> + <note>21: Site 1 annotation for 2012-02-10</note> + <starred>1</starred> + <user>superUserLogin</user> + <canEditOrDelete>1</canEditOrDelete> + <idNote>43</idNote> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/System/expected/test_annotations_range__Annotations.getAnnotationCountForDates_range.xml b/plugins/Annotations/tests/System/expected/test_annotations_range__Annotations.getAnnotationCountForDates_range.xml new file mode 100644 index 0000000000000000000000000000000000000000..77c83c9c846d7880a9ffe0f6944dcb9bbe071446 --- /dev/null +++ b/plugins/Annotations/tests/System/expected/test_annotations_range__Annotations.getAnnotationCountForDates_range.xml @@ -0,0 +1,229 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row key="1"> + <row> + <row>2012-01-15</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-16</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-17</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-18</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-19</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-20</row> + <row> + <count>1</count> + <starred>1</starred> + </row> + </row> + <row> + <row>2012-01-21</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-22</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-23</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-24</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-25</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-26</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-27</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-28</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-29</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-30</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-01-31</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-01</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-02</row> + <row> + <count>1</count> + <starred>1</starred> + </row> + </row> + <row> + <row>2012-02-03</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-04</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-05</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-06</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-07</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-08</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-09</row> + <row> + <count>1</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-10</row> + <row> + <count>1</count> + <starred>1</starred> + </row> + </row> + <row> + <row>2012-02-11</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-12</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-13</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-14</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + <row> + <row>2012-02-15</row> + <row> + <count>0</count> + <starred>0</starred> + </row> + </row> + </row> +</result> \ No newline at end of file diff --git a/plugins/Annotations/tests/UI/Annotations_spec.js b/plugins/Annotations/tests/UI/Annotations_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..d5564e2ebe3b52049fc9e2185f99e3ff063de9a3 --- /dev/null +++ b/plugins/Annotations/tests/UI/Annotations_spec.js @@ -0,0 +1,82 @@ +/*! + * Matomo - free/libre analytics platform + * + * Screenshot tests for Marketplace. + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe("Annotations", function () { + this.fixture = "Piwik\\Tests\\Fixtures\\TwoSitesWithAnnotations"; + + const generalParams = "idSite=1&period=day&date=2012-04-01"; + const category = "&category=General_Visitors&subcategory=General_Overview"; + const url = "?module=CoreHome&action=index&" + generalParams + category; + + it("should show annotations", async function() { + await page.goto(url); + await page.waitForNetworkIdle(); + + await page.click(".evolution-annotations span[title^=\"View and add annotations\"]"); + await page.waitForTimeout(200); + + expect(await page.screenshotSelector(".isFirstWidgetInPage .annotation-manager")).to.matchImage("list_annotations"); + }); + + it("should add annotation", async function() { + await page.click(".add-annotation"); + await page.waitForTimeout(100); + + await page.type(".new-annotation-edit", "<h2>x</h2><script>alert(5)</script>{{ 2+2 }}"); + await page.click(".new-annotation-save"); + await page.waitForNetworkIdle(); + + expect(await page.screenshotSelector(".isFirstWidgetInPage .annotation-manager")).to.matchImage("added_annotation"); + }); + + // Adding an annotation with a range selected used to fail because + // the inherited period=range made AjaxHelper reject the single annotation date. + it("should add annotation when a date range is selected", async function() { + const rangeParams = "idSite=1&period=range&date=2012-03-01,2012-04-01"; + await page.goto("?module=CoreHome&action=index&" + rangeParams + category); + await page.waitForNetworkIdle(); + + await page.click(".evolution-annotations span[title^=\"View and add annotations\"]"); + await page.waitForSelector(".add-annotation", { visible: true }); + + await page.click(".add-annotation"); + await page.waitForSelector(".new-annotation-edit", { visible: true }); + + await page.type(".new-annotation-edit", "range period annotation"); + await page.click(".new-annotation-save"); + await page.waitForNetworkIdle(); + + // the manager only shows the note if the save succeeded + const managerText = await page.evaluate(function () { + return document.querySelector(".isFirstWidgetInPage .annotation-manager").innerText; + }); + expect(managerText).to.contain("range period annotation"); + }); + + // Same fix for the edit path: saving an edited annotation also sends a single date. + it("should save an edited annotation when a date range is selected", async function() { + // still on the range page from the previous test; edit the first annotation + const annotation = ".isFirstWidgetInPage .annotation-manager .annotation"; + + await page.click(annotation + " .annotation-value .annotation-enter-edit-mode"); + await page.waitForSelector(annotation + " .annotation-edit", { visible: true }); + + // change the note text so the save isn't skipped as a no-op + await page.click(annotation + " .annotation-edit", { clickCount: 3 }); + await page.type(annotation + " .annotation-edit", "edited range annotation"); + await page.click(annotation + " .annotation-edit-mode .annotation-save"); + await page.waitForNetworkIdle(); + + // the manager only shows the new text if the save succeeded + const managerText = await page.evaluate(function () { + return document.querySelector(".isFirstWidgetInPage .annotation-manager").innerText; + }); + expect(managerText).to.contain("edited range annotation"); + }); +}); diff --git a/plugins/Annotations/tests/UI/expected-screenshots/Annotations_added_annotation.png b/plugins/Annotations/tests/UI/expected-screenshots/Annotations_added_annotation.png new file mode 100644 index 0000000000000000000000000000000000000000..3c75297b1ffdbc5d61cdfd4977d64067b54b59fe Binary files /dev/null and b/plugins/Annotations/tests/UI/expected-screenshots/Annotations_added_annotation.png differ diff --git a/plugins/Annotations/tests/UI/expected-screenshots/Annotations_list_annotations.png b/plugins/Annotations/tests/UI/expected-screenshots/Annotations_list_annotations.png new file mode 100644 index 0000000000000000000000000000000000000000..033177a45cdb907e3ff4b0db67eb6dee981b836a Binary files /dev/null and b/plugins/Annotations/tests/UI/expected-screenshots/Annotations_list_annotations.png differ diff --git a/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/matomo-ai-checklist.yml b/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/matomo-ai-checklist.yml new file mode 100644 index 0000000000000000000000000000000000000000..cc2c022da6f944b9683668c262330ca060d15f8b --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/matomo-ai-checklist.yml @@ -0,0 +1,32 @@ +# Action for running tests + +name: AI Checklist + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + actions: read + checks: none + contents: none + deployments: none + issues: none + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: none + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + AiChecklist: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + steps: + - name: Run tests + uses: matomo-org/github-action-checklist-gate@main diff --git a/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/matomo-tests.yml b/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/matomo-tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..18e35c4f5269633b85cfcb88aadaf66c1cfb2c96 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/matomo-tests.yml @@ -0,0 +1,94 @@ +# Action for running tests +# This file has been automatically created. +# To recreate it you can run this command +# ./console generate:test-action --plugin="AnonymousPiwikUsageMeasurement" --php-versions="matomo5_min_php,matomo5_max_php" --schedule-cron="20 2 * * 6" + +name: Plugin AnonymousPiwikUsageMeasurement Tests + +on: + pull_request: + types: [opened, synchronize] + push: + branches: + - '**.x-dev' + workflow_dispatch: + schedule: + - cron: "20 2 * * 6" + +permissions: + actions: read + checks: none + contents: read + deployments: none + issues: read + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: none + +concurrency: + group: php-${{ github.ref }} + cancel-in-progress: true + +jobs: + PluginTests: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: [ 'matomo5_min_php', 'matomo5_max_php' ] + target: ['minimum_required_matomo', 'maximum_supported_matomo'] + steps: + - uses: actions/checkout@v3 + with: + lfs: true + persist-credentials: false + - name: Install package ripgrep + run: sudo apt-get install ripgrep + - name: Run tests + uses: matomo-org/github-action-tests@main + with: + plugin-name: 'AnonymousPiwikUsageMeasurement' + php-version: ${{ matrix.php }} + test-type: 'PluginTests' + matomo-test-branch: ${{ matrix.target }} + artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} + upload-artifacts: ${{ matrix.php == 'matomo5_min_php' && matrix.target == 'maximum_supported_matomo' }} + Client: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + target: ['minimum_required_matomo', 'maximum_supported_matomo'] + steps: + - uses: actions/checkout@v3 + with: + lfs: false + persist-credentials: false + - name: running tests + uses: matomo-org/github-action-tests@main + with: + plugin-name: 'AnonymousPiwikUsageMeasurement' + matomo-test-branch: ${{ matrix.target }} + test-type: 'Client' + node-version: '16' + mysql-service: false + UI: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + with: + lfs: true + persist-credentials: false + - name: running tests + uses: matomo-org/github-action-tests@main + with: + plugin-name: 'AnonymousPiwikUsageMeasurement' + matomo-test-branch: 'maximum_supported_matomo' + test-type: 'UI' + php-version: 'matomo5_min_php' + node-version: '16' + artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} + upload-artifacts: true diff --git a/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/phpcs.yml b/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/phpcs.yml new file mode 100644 index 0000000000000000000000000000000000000000..fe35d078aacd396fb5796fb075ece291077bdea8 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/.github/workflows/phpcs.yml @@ -0,0 +1,43 @@ +name: PHPCS check + +on: pull_request + +permissions: + actions: read + checks: read + contents: read + deployments: none + issues: read + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: read + +jobs: + phpcs: + name: PHPCS + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + lfs: false + persist-credentials: false + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: cs2pr + - name: Install dependencies + run: + composer init --name=matomo/anonymouspiwikusagemeasurement --quiet; + composer --no-plugins config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true -n; + composer config repositories.matomo-coding-standards vcs https://github.com/matomo-org/matomo-coding-standards -n; + composer require matomo-org/matomo-coding-standards:dev-master; + composer install --dev --prefer-dist --no-progress --no-suggest + - name: Check PHP code styles + id: phpcs + run: ./vendor/bin/phpcs --report-full --standard=phpcs.xml --report-checkstyle=./phpcs-report.xml + - name: Show PHPCS results in PR + if: ${{ always() && steps.phpcs.outcome == 'failure' }} + run: cs2pr ./phpcs-report.xml --prepend-filename diff --git a/plugins/AnonymousPiwikUsageMeasurement/.gitignore b/plugins/AnonymousPiwikUsageMeasurement/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..71f26935a1cecacd224909f8279b3ed3c70fe634 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/.gitignore @@ -0,0 +1,6 @@ +tests/System/processed/*xml +tests/System/processed/*text +/vue/dist/demo.html +/vue/dist/*.common.js +/vue/dist/*.map +/vue/dist/*.development.* diff --git a/plugins/AnonymousPiwikUsageMeasurement/AnonymousPiwikUsageMeasurement.php b/plugins/AnonymousPiwikUsageMeasurement/AnonymousPiwikUsageMeasurement.php new file mode 100644 index 0000000000000000000000000000000000000000..d9c711b524a3abde4f12f2c8194fd565c34cd9c3 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/AnonymousPiwikUsageMeasurement.php @@ -0,0 +1,139 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement; + +use Piwik\Common; +use Piwik\Container\StaticContainer; +use Piwik\Date; +use Piwik\Piwik; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Profiles; + +class AnonymousPiwikUsageMeasurement extends \Piwik\Plugin +{ + public const TRACKING_DOMAIN = 'https://demo-anonymous.matomo.org'; + public const EXAMPLE_DOMAIN = 'http://example.com'; + + private $profilingStack = array(); + + /** + * @see \Piwik\Plugin::registerEvents + */ + public function registerEvents() + { + return array( + 'AssetManager.getJavaScriptFiles' => 'getJsFiles', + 'Template.jsGlobalVariables' => 'addMatomoClientTracking', + 'API.Request.dispatch' => 'logStartTimeOfApiCall', + 'API.Request.dispatch.end' => 'trackApiCall', + 'Db.getTablesInstalled' => 'getTablesInstalled' + ); + } + + public function install() + { + $dao = new Profiles(); + $dao->install(); + } + + public function uninstall() + { + $dao = new Profiles(); + $dao->uninstall(); + } + + /** + * Register the new tables, so Matomo knows about them. + * + * @param array $allTablesInstalled + */ + public function getTablesInstalled(&$allTablesInstalled) + { + $allTablesInstalled[] = Common::prefixTable('usage_measurement_profiles'); + } + + public function logStartTimeOfApiCall(&$finalParameters, $pluginName, $methodName) + { + // API methods can call other API methods... + $this->profilingStack[] = array( + 'method' => $pluginName . '.' . $methodName, + 'time' => microtime(true) + ); + } + + public function trackApiCall(&$return, $endHookParams) + { + $endTime = microtime(true); + + $name = $endHookParams['module']; + $method = $name . '.' . $endHookParams['action']; + $neededTimeInMs = 0; + + do { + $call = array_pop($this->profilingStack); + + // we need to make sure the call was actually for this method to not send wrong data. + if ($method === $call['method']) { + $neededTimeInMs = ceil(($endTime - $call['time']) * 1000); + break; + } + } while (!empty($this->profilingStack)); + + if (empty($neededTimeInMs)) { + return; + } + + $profiles = StaticContainer::get('Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Profiles'); + + $now = Date::now()->getDatetime(); + $category = 'API'; + + $profiles->pushProfile($now, $category, $name, $method, $count = 1, (int) $neededTimeInMs); + } + + public function getJsFiles(&$jsFiles) + { + $jsFiles[] = 'matomo.js'; + $jsFiles[] = 'plugins/AnonymousPiwikUsageMeasurement/javascripts/url.js'; + $jsFiles[] = 'plugins/AnonymousPiwikUsageMeasurement/javascripts/tracking.js'; + } + + public function addMatomoClientTracking(&$out) + { + $settings = StaticContainer::get(SystemSettings::class); + $userSettings = StaticContainer::get(UserSettings::class); + + $config = array( + 'targets' => array(), + 'visitorCustomVariables' => array(), + 'trackingDomain' => self::TRACKING_DOMAIN, + 'exampleDomain' => self::EXAMPLE_DOMAIN, + 'userId' => Piwik::getCurrentUserLogin() + ); + + if ( + Piwik::isUserIsAnonymous() + || !$settings->canUserOptOut->getValue() + || $userSettings->userTrackingEnabled->getValue() + ) { + // an anonymous user is currently always tracked, an anonymous user would not have permission to read + // this user setting. The `isUserIsAnonymous()` check is not needed but there to improve performance + // in case user is anonymous. Then we avoid checking whether user has access to any sites which can be slow + // a user not having any view permission is also always tracked so far as such a user is not allowed to read + // this setting + $targets = StaticContainer::get('Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Targets'); + $customVars = StaticContainer::get('Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\CustomVariables'); + + $config['targets'] = $targets->getTargets(); + $config['visitorCustomVariables'] = $customVars->getClientVisitCustomVariables(); + } + + $out .= "\nvar piwikUsageTracking = " . json_encode($config) . ";\n"; + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/CHANGELOG.md b/plugins/AnonymousPiwikUsageMeasurement/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..f80e660e530ef4cfca522454fcc832ea588e56a5 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/CHANGELOG.md @@ -0,0 +1,19 @@ +## Changelog + +* 5.0.2 - 2025-07-07 - Textual changes +* 5.0.1 Added plugin category for Marketplace +* 5.0.0 Compatibility with Matomo 5 +* 4.1.0 Migrating from AngularJS to Vue +* 4.0.2 Compatibility with Matomo 4 +* 4.0.1 Compatibility with Matomo 4 +* 4.0.0 Compatibility with Matomo 4 +* 3.1.0 No longer send optionally any usage data to Matomo +* 3.0.3 Renaming from Piwik to Matomo +* 3.0.0 Compatibility with Matomo 3 +* 0.2.1 Track MySQL server version in a custom variable. +* 0.2.0 Add possibility to enable/disable anonymization and tracking user login as userID +* 0.1.4 Fixed a bug that failed to track under HTTPS under circumstances +* 0.1.3 Updated plugin description only +* 0.1.2 Bugfixes +* 0.1.1 Track average API executime time +* 0.1.0 Initial release diff --git a/plugins/AnonymousPiwikUsageMeasurement/LICENSE b/plugins/AnonymousPiwikUsageMeasurement/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/plugins/AnonymousPiwikUsageMeasurement/README.md b/plugins/AnonymousPiwikUsageMeasurement/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9a459a120181f262efae1a9f29003f2dbace9b4b --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/README.md @@ -0,0 +1,36 @@ +# Matomo AnonymousPiwikUsageMeasurement Plugin + +[![Build Status](https://github.com/matomo-org/plugin-AnonymousPiwikUsageMeasurement/actions/workflows/matomo-tests.yml/badge.svg?branch=4.x-dev)](https://github.com/matomo-org/plugin-AnonymousPiwikUsageMeasurement/actions/workflows/matomo-tests.yml) + +## Description + +Track usage of your Matomo service into up to two Matomos: + +* your own Matomo (can be configured optionally) +* a custom Matomo (can be configured optionally) + +### What is Matomo doing to make sure the data is anonymized? + +We are very careful in what we track and we make sure to anonymize data that could contain user data. + +* We overwrite the page title as the title could contain the name of the viewed website +* We remove any referrer information +* We replace URL parameters with a predefined value apart from a few whitelisted ones to make sure no actual token_auth, CSRF token or user defined value will be tracked +* We do not just track any outlinks or downloads + +We track any information as efficient as possible to not slow down your Matomo. If you have already performance problems with your Matomo we recommend to not install this plugin though. + +### Which data is tracked? + +When the plugin is activated, the following data will be tracked: + +* The pages and reports that are viewed +* The visitors' software and devices data like the used browser and the resolution +* Some clicks or interactions with certain selectors or buttons. For example we track an event when a segment is selected but we do not track the actual segment. +* In a daily task we track the following data: + * Matomo version + * PHP version + * Number of websites + * Number of users + * Number of segments + * How often which API method was called (only plugin name and method name but no parameters) and how long the API calls took on average. diff --git a/plugins/AnonymousPiwikUsageMeasurement/SystemSettings.php b/plugins/AnonymousPiwikUsageMeasurement/SystemSettings.php new file mode 100644 index 0000000000000000000000000000000000000000..c9071e2a679083ffbd5a37f70e2ddee5ee7d16ad --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/SystemSettings.php @@ -0,0 +1,158 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement; + +use Piwik\API\Request; +use Piwik\Common; +use Exception; +use Piwik\Settings\Setting; +use Piwik\Settings\FieldConfig; +use Piwik\UrlHelper; + +/** + * Defines Settings for AnonymousPiwikUsageMeasurement. + */ +class SystemSettings extends \Piwik\Settings\Plugin\SystemSettings +{ + /** @var Setting */ + public $canUserOptOut; + + /** @var Setting */ + public $ownPiwikSiteId; + + /** @var Setting */ + public $customPiwikSiteId; + + /** @var Setting */ + public $customPiwikSiteUrl; + + /** @var Setting */ + public $anonymizeCustomPiwik; + + /** @var Setting */ + public $anonymizeSelfPiwik; + + protected function init() + { + $this->canUserOptOut = $this->createLetUsersOptOutSetting(); + + $this->ownPiwikSiteId = $this->createTrackToOwnPiwikSetting(); + $this->anonymizeSelfPiwik = $this->anonymizationOption('anonymizeSelfPiwik'); + + $this->customPiwikSiteUrl = $this->createTrackToCustomSiteUrlSetting(); + $this->customPiwikSiteId = $this->createTrackToCustomSiteIdSetting(); + + $this->anonymizeCustomPiwik = $this->anonymizationOption('anonymizeCustomPiwik'); + } + + private function createLetUsersOptOutSetting() + { + return $this->makeSetting('canUserOptOut', $default = true, FieldConfig::TYPE_BOOL, function (FieldConfig $field) { + $field->title = 'Let users disable anonymous tracking'; + $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX; + $field->description = 'If enabled, logged in users can opt out in their plugin settings. Anonymous users cannot opt out.'; + }); + } + + private function createTrackToOwnPiwikSetting() + { + return $this->makeSetting('ownPiwikSiteId', $default = 0, FieldConfig::TYPE_INT, function (FieldConfig $field) { + $field->title = 'Site Id'; + // ideally we would use a SELECT control and let user choose an existing site but this would make performance slow + // since we'd always have to get all site ids in each request + $field->uiControl = FieldConfig::UI_CONTROL_TEXT; + $field->introduction = 'Send usage data to this Matomo'; + $field->description = 'If specified, anonymized usage data will be sent to the specified site in this Matomo. This lets you analyze how you and your colleagues are using Matomo.'; + $field->validate = function ($idSite) { + if (empty($idSite)) { + return; + } + + if (!is_numeric($idSite)) { + throw new Exception("Site Id '$idSite' should be a number"); + } + + $idSite = (int) $idSite; + try { + $siteExists = Request::processRequest('SitesManager.getSiteFromId', array('idSite' => $idSite)); + } catch (Exception $e) { + $siteExists = false; + } + + if (!$siteExists) { + throw new Exception("The specified idSite '$idSite' does not exist"); + } + }; + }); + } + + private function createTrackToCustomSiteUrlSetting() + { + return $this->makeSetting('customSiteUrl', $default = '', FieldConfig::TYPE_STRING, function (FieldConfig $field) { + $field->title = 'Matomo Url'; + $field->uiControl = FieldConfig::UI_CONTROL_TEXT; + $field->uiControlAttributes = array('placeHolder' => 'eg. http://example.com/matomo'); + $field->introduction = 'Send usage data to a custom Matomo'; + $field->description = ''; + $field->validate = function ($value, $setting) { + if (empty($value)) { + return; + } + + if (!UrlHelper::isLookLikeUrl($value)) { + throw new Exception("URL '$value' seems to be not a valid URL"); + } + + // TODO should we check if URL exists and is valid?!? might not work if instance is not connected to internet + }; + $field->transform = function ($value) { + if (empty($value)) { + return ''; + } + + if (!Common::stringEndsWith($value, '/piwik.php') && !Common::stringEndsWith($value, '/matomo.php')) { + if (!Common::stringEndsWith($value, '/')) { + $value .= '/'; + } + $value .= 'matomo.php'; + } + + return $value; + }; + }); + } + + private function createTrackToCustomSiteIdSetting() + { + return $this->makeSetting('customSiteId', $default = 0, FieldConfig::TYPE_INT, function (FieldConfig $field) { + $field->title = 'Site Id'; + $field->uiControl = FieldConfig::UI_CONTROL_TEXT; + $field->uiControlAttributes = array('placeHolder' => 'eg. "2"'); + $field->description = 'If a URL and Site Id is specified, usage data will be sent to the custom Matomo instance.'; + $field->validate = function ($idSite) { + if (empty($idSite)) { + return; + } + + if (!is_numeric($idSite)) { + throw new Exception("Site Id '$idSite' should be a number"); + } + }; + }); + } + + private function anonymizationOption($optionName) + { + return $this->makeSetting($optionName, $default = true, FieldConfig::TYPE_BOOL, function (FieldConfig $field) { + $field->title = 'Anonymize tracking requests'; + $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX; + }); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/Tasks.php b/plugins/AnonymousPiwikUsageMeasurement/Tasks.php new file mode 100644 index 0000000000000000000000000000000000000000..918912e6ce478a7b33ba929967b04a173ee548a2 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/Tasks.php @@ -0,0 +1,82 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement; + +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\CustomVariables; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Profiles; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Trackers; + +class Tasks extends \Piwik\Plugin\Tasks +{ + /** + * @var Trackers + */ + private $trackers; + + /** + * @var CustomVariables + */ + private $customVars; + + /** + * @var Profiles + */ + private $profiles; + + public function __construct(Trackers $trackers, CustomVariables $customVars, Profiles $profiles) + { + $this->trackers = $trackers; + $this->customVars = $customVars; + $this->profiles = $profiles; + } + + public function schedule() + { + $scheduledTime = $this->daily('sendSystemReport'); + $scheduledTime->setHour(rand(1, 23)); // make sure not all Piwik instances send data to matomo.org at same time + } + + /** + * To test execute the following command: + * `./console core:run-scheduled-tasks "Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tasks.sendSystemReport"` + * + * @throws \Exception + */ + public function sendSystemReport() + { + $trackers = $this->trackers->makeTrackers(); + $customVars = $this->customVars->getServerVisitCustomVariables(); + $profiles = $this->profiles->popAll(); + + foreach ($trackers as $tracker) { + $tracker->enableBulkTracking(); + + foreach ($customVars as $customVar) { + $tracker->setCustomVariable($customVar['id'], $customVar['name'], $customVar['value'], 'visit'); + } + + $tracker->setAnonymousUrl('/system-report'); + $tracker->doTrackPageView('System-Report'); + + foreach ($profiles as $profile) { + $countAppendix = ' (count)'; + $tracker->doTrackEvent($profile['category'] . $countAppendix, $profile['action'] . $countAppendix, $profile['name'] . $countAppendix, $profile['count']); + + if ($profile['count'] > 0) { + $speedAppendix = ' (speed)'; + $wallTimeAvg = round($profile['wall_time'] / $profile['count'], 1); + $tracker->doTrackEvent($profile['category'] . $speedAppendix, $profile['action'] . $speedAppendix, $profile['name'] . $speedAppendix, $wallTimeAvg); + } + } + + $tracker->doBulkTrack(); + } + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/Tracker.php b/plugins/AnonymousPiwikUsageMeasurement/Tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..d72ce83ac35a27bbdea40dbbbacd41ef196246e9 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/Tracker.php @@ -0,0 +1,45 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement; + +/** + * The default PiwikTracker cannot handle multiple instances to different URLs see + * https://github.com/piwik/piwik-php-tracker/issues/14 + */ +class Tracker extends \MatomoTracker +{ + private $baseApiUrl; + + /** + * The method name is not ideal but want to make sure MatomoTracker won't define such method at some point. + * @param string $baseApiUrl eg 'http://demo-anonymous.matomo.org/matomo.php' + */ + public function setBaseApiUrl($baseApiUrl) + { + $this->baseApiUrl = $baseApiUrl; + } + + public function setAnonymousUrl($path = '') + { + $this->setUrl(AnonymousPiwikUsageMeasurement::TRACKING_DOMAIN . $path); + } + + /** + * Returns the base URL for the piwik server. + */ + protected function getBaseUrl() + { + if (!empty($this->baseApiUrl)) { + return $this->baseApiUrl; + } + + return parent::getBaseUrl(); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/Tracker/CustomVariables.php b/plugins/AnonymousPiwikUsageMeasurement/Tracker/CustomVariables.php new file mode 100644 index 0000000000000000000000000000000000000000..6b72033d713bcf1d15e5ccd6d0cf695a42ac727f --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/Tracker/CustomVariables.php @@ -0,0 +1,96 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker; + +use Piwik\API\Request; +use Piwik\Container\StaticContainer; +use Piwik\Db; +use Piwik\Piwik; +use Piwik\Plugins\SegmentEditor\Services\StoredSegmentService; + +class CustomVariables +{ + /** + * We send this data via server to not expose eg PHP version to users + * @return array + */ + public function getServerVisitCustomVariables() + { + $users = Request::processRequest('UsersManager.getUsers', array('filter_limit' => '-1')); + $websites = Request::processRequest('SitesManager.getAllSites', array('filter_limit' => '-1')); + + $db = Db::get(); + $mySqlVersion = null; + if (method_exists($db, 'getServerVersion')) { + $mySqlVersion = $db->getServerVersion(); + } + + $customVars = array( + array( + 'id' => 1, + 'name' => 'Matomo Version', + 'value' => StaticContainer::get('AnonymousPiwikUsageMeasurement.piwikVersion'), + ), + array( + 'id' => 2, + 'name' => 'PHP Version', + 'value' => StaticContainer::get('AnonymousPiwikUsageMeasurement.phpVersion'), + ), + array( + 'id' => 3, + 'name' => 'Num Users', + 'value' => count($users), + ), + array( + 'id' => 4, + 'name' => 'Num Websites', + 'value' => count($websites), + ), + ); + + $service = StaticContainer::get(StoredSegmentService::class); + $segments = $service->getAllSegmentsAndIgnoreVisibility(); + $customVars[] = array( + 'id' => 5, + 'name' => 'Num Segments', + 'value' => count($segments), + ); + + if (isset($mySqlVersion)) { + $customVars[] = array( + 'id' => 6, + 'name' => 'MySQL Version', + 'value' => $mySqlVersion, + ); + } + + return $customVars; + } + + public function getClientVisitCustomVariables() + { + if (Piwik::hasUserSuperUserAccess()) { + $access = 'superuser'; + } elseif (Piwik::isUserIsAnonymous()) { + $access = 'anonymous'; + } else { + // I do not check between view/admin as it could trigger slow DB queries to fetch sites with access + $access = 'user'; + } + + return array( + array( + 'id' => 1, + 'name' => 'Access', + 'value' => $access, + ) + ); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/Tracker/Profiles.php b/plugins/AnonymousPiwikUsageMeasurement/Tracker/Profiles.php new file mode 100644 index 0000000000000000000000000000000000000000..48d24288f4e08a353c40f4e7e862516e3ee7867a --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/Tracker/Profiles.php @@ -0,0 +1,62 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker; + +use Piwik\Common; +use Piwik\Db; +use Piwik\DbHelper; + +class Profiles +{ + private $tableName = 'usage_measurement_profiles'; + private $tableNamePrefixed; + + public function __construct() + { + $this->tableNamePrefixed = Common::prefixTable($this->tableName); + } + + public function popAll() + { + $profiles = Db::get()->fetchAll('SELECT * FROM ' . $this->tableNamePrefixed); + Db::get()->query('DELETE FROM ' . $this->tableNamePrefixed); + + return $profiles; + } + + public function pushProfile($creationDate, $category, $name, $action, $count, $wallTimeInMs) + { + $sql = 'INSERT INTO `' . $this->tableNamePrefixed . + '` (creation_date, `category`, `name`, `action`, `count`, `wall_time`) ' . + ' VALUES (?, ?, ?, ?, ?, ?) ' . + ' ON DUPLICATE KEY UPDATE `count` = `count` + ?, `wall_time` = `wall_time` + ?'; + $bind = array($creationDate, $category, $name, $action, $count, $wallTimeInMs, $count, $wallTimeInMs); + + Db::get()->query($sql, $bind); + } + + public function install() + { + $table = "`creation_date` datetime NOT NULL , + `category` VARCHAR(200) NOT NULL , + `name` VARCHAR(200) NOT NULL , + `action` VARCHAR(200) NOT NULL, + `count` INT UNSIGNED NOT NULL DEFAULT 0 , + `wall_time` BIGINT UNSIGNED NOT NULL DEFAULT 0 , + PRIMARY KEY(`category`(64), `name`(64), `action`(63))"; + + DbHelper::createTable($this->tableName, $table); + } + + public function uninstall() + { + Db::dropTables(array($this->tableNamePrefixed)); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/Tracker/Targets.php b/plugins/AnonymousPiwikUsageMeasurement/Tracker/Targets.php new file mode 100644 index 0000000000000000000000000000000000000000..0f64d198432fae5af8f8dc133017c2622f65b264 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/Tracker/Targets.php @@ -0,0 +1,67 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker; + +use Piwik\Common; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings; +use Piwik\SettingsPiwik; +use Piwik\Tests\Framework\Fixture; + +/** + * Defines Settings for AnonymousPiwikUsageMeasurement. + */ +class Targets +{ + /** + * @var SystemSettings + */ + private $settings; + + public function __construct(SystemSettings $settings) + { + $this->settings = $settings; + } + + public function getTargets() + { + $targets = array(); + + $ownSiteId = $this->settings->ownPiwikSiteId->getValue(); + if ($ownSiteId) { + $piwikUrl = SettingsPiwik::getPiwikUrl(); + if (!Common::stringEndsWith($piwikUrl, '/')) { + $piwikUrl .= '/'; + } + $targets[] = array( + 'url' => $piwikUrl . 'matomo.php', + 'idSite' => (int) $ownSiteId, + 'useAnonymization' => $this->settings->anonymizeSelfPiwik->getValue() + ); + } + + $customUrl = $this->settings->customPiwikSiteUrl->getValue(); + $customSiteId = $this->settings->customPiwikSiteId->getValue(); + if ($customUrl && $customSiteId) { + $targets[] = array( + 'url' => $customUrl, + 'idSite' => (int) $customSiteId, + 'useAnonymization' => $this->settings->anonymizeCustomPiwik->getValue() + ); + } + + if (defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE && Common::isPhpCliMode()) { + foreach ($targets as &$target) { + $target['token_auth'] = Fixture::getTokenAuth(); + } + } + + return $targets; + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/Tracker/Trackers.php b/plugins/AnonymousPiwikUsageMeasurement/Tracker/Trackers.php new file mode 100644 index 0000000000000000000000000000000000000000..d2139399b95e3dbcb04b6cc43ec2fb1edd0fe106 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/Tracker/Trackers.php @@ -0,0 +1,49 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker; + +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker; + +class Trackers +{ + /** + * @var Targets + */ + private $targets; + + public function __construct(Targets $targets) + { + $this->targets = $targets; + } + + /** + * We send this data via server to not expose eg PHP version to users + * @return Tracker[] + */ + public function makeTrackers() + { + $trackers = array(); + $targets = $this->targets->getTargets(); + + foreach ($targets as $target) { + $tracker = new Tracker($target['idSite'], $target['url']); + $tracker->setBaseApiUrl($target['url']); + $tracker->setRequestTimeout($seconds = 15); + $tracker->setAnonymousUrl(); + $tracker->setUrlReferrer(''); + if (isset($target['token_auth'])) { + $tracker->setTokenAuth($target['token_auth']); + } + $trackers[] = $tracker; + } + + return $trackers; + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/Updates/0.1.1.php b/plugins/AnonymousPiwikUsageMeasurement/Updates/0.1.1.php new file mode 100644 index 0000000000000000000000000000000000000000..54dcdbbce282f0fc5676a10d4de057ed132b94f2 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/Updates/0.1.1.php @@ -0,0 +1,79 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement; + +use Piwik\Updater; +use Piwik\Updates as PiwikUpdates; +use Piwik\Updater\Migration\Factory as MigrationFactory; + +/** + * Update for version 0.1.1. + */ +class Updates_0_1_1 extends PiwikUpdates +{ + /** + * @var MigrationFactory + */ + private $migration; + + public function __construct(MigrationFactory $factory) + { + $this->migration = $factory; + } + + /** + * Return SQL to be executed in this update. + * + * SQL queries should be defined here, instead of in `doUpdate()`, since this method is used + * in the `core:update` command when displaying the queries an update will run. If you execute + * queries directly in `doUpdate()`, they won't be displayed to the user. + * + * @param Updater $updater + * @return array ``` + * array( + * 'ALTER .... ' => '1234', // if the query fails, it will be ignored if the error code is 1234 + * 'ALTER .... ' => false, // if an error occurs, the update will stop and fail + * // and user will have to manually run the query + * ) + * ``` + */ + public function getMigrations(Updater $updater) + { + $sqls = array( + $this->migration->db->dropTable('usage_measurement_events'), + $this->migration->db->createTable('usage_measurement_profiles', array( + 'creation_date' => 'datetime NOT NULL', + 'category' => 'VARCHAR(200) NOT NULL', + 'name' => 'VARCHAR(200) NOT NULL', + 'action' => 'VARCHAR(200) NOT NULL', + 'count' => 'INT UNSIGNED NOT NULL DEFAULT 0' , + 'wall_time' => 'BIGINT UNSIGNED NOT NULL DEFAULT 0' , + ), $primary = array('category', 'name', 'action')) + ); + + return $sqls; + } + + /** + * Perform the incremental version update. + * + * This method should perform all updating logic. If you define queries in an overridden `getMigrationQueries()` + * method, you must call {@link Updater::executeMigrationQueries()} here. + * + * See {@link Updates} for an example. + * + * @param Updater $updater + */ + public function doUpdate(Updater $updater) + { + $updater->executeMigrations(__FILE__, $this->getMigrations($updater)); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/Updates/4.0.0.php b/plugins/AnonymousPiwikUsageMeasurement/Updates/4.0.0.php new file mode 100644 index 0000000000000000000000000000000000000000..1d8c27912cfd96dca55b1fba89e94762a834d132 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/Updates/4.0.0.php @@ -0,0 +1,54 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement; + +use Piwik\Common; +use Piwik\DbHelper; +use Piwik\Updater; +use Piwik\Updates as PiwikUpdates; +use Piwik\Updater\Migration\Factory as MigrationFactory; + +/** + * Update for version 4.0.0. + */ +class Updates_4_0_0 extends PiwikUpdates +{ + /** + * @var MigrationFactory + */ + private $migration; + + public function __construct(MigrationFactory $factory) + { + $this->migration = $factory; + } + + public function getMigrations(Updater $updater) + { + $migrations = []; + + if ('utf8mb4' === DbHelper::getDefaultCharset()) { + $migrations[] = $this->migration->db->dropPrimaryKey('usage_measurement_profiles'); + $migrations[] = $this->migration->db->sql(sprintf( + 'ALTER TABLE `%s` ADD PRIMARY KEY(%s)', + Common::prefixTable('usage_measurement_profiles'), + implode(', ', ['`category`(64)', '`name`(64)', '`action`(63)']) + )); + } + + return $migrations; + } + + public function doUpdate(Updater $updater) + { + $updater->executeMigrations(__FILE__, $this->getMigrations($updater)); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/UserSettings.php b/plugins/AnonymousPiwikUsageMeasurement/UserSettings.php new file mode 100644 index 0000000000000000000000000000000000000000..b278384cd9be8fcfb23f0e6b7de0500b2052165e --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/UserSettings.php @@ -0,0 +1,52 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement; + +use Piwik\Settings\Setting; +use Piwik\Settings\FieldConfig; + +/** + * Defines Settings for AnonymousPiwikUsageMeasurement. + */ +class UserSettings extends \Piwik\Settings\Plugin\UserSettings +{ + /** @var Setting */ + public $userTrackingEnabled; + + /** + * @var SystemSettings + */ + private $systemSettings; + + public function __construct(SystemSettings $systemSettings) + { + $this->systemSettings = $systemSettings; + parent::__construct(); + } + + protected function init() + { + $this->userTrackingEnabled = $this->createUsersOptOutSetting(); + + if (!$this->systemSettings->canUserOptOut->getValue()) { + // we show this setting only when a user can actually opt out + $this->userTrackingEnabled->setIsWritableByCurrentUser(false); + } + } + + private function createUsersOptOutSetting() + { + return $this->makeSetting('userTrackingEnabled', $default = true, FieldConfig::TYPE_BOOL, function (FieldConfig $field) { + $field->title = 'Matomo usage tracking enabled'; + $field->uiControl = FieldConfig::UI_CONTROL_CHECKBOX; + $field->description = 'If enabled, anonymous usage data will be tracked. For example which pages are viewed and which reports are used most often. For more information contact your system administrator.'; + }); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/config/config.php b/plugins/AnonymousPiwikUsageMeasurement/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..ed8b78dfb71800a8085f319b9944d04eaed077b3 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/config/config.php @@ -0,0 +1,26 @@ +<?php + +return array( + 'AnonymousPiwikUsageMeasurement.piwikVersion' => \Piwik\Version::VERSION, + 'AnonymousPiwikUsageMeasurement.phpVersion' => phpversion(), + 'Piwik\View\SecurityPolicy' => Piwik\DI::decorate(function ($previous) { + /** @var \Piwik\View\SecurityPolicy $previous */ + + if (!\Piwik\SettingsPiwik::isMatomoInstalled()) { + // if Matomo is not yet installed there can't be any system setting + return $previous; + } + + try { + $settings = new Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings(); + $customSiteUrl = $settings->getSetting('customSiteUrl')->getValue(); + $host = parse_url($customSiteUrl, PHP_URL_HOST); + if (!empty($customSiteUrl) && !empty($host)) { + $previous->addPolicy('default-src', $host); + } + } catch (\Exception $e) { + // ignore possible errors as they might break Matomo + } + return $previous; + }) +); diff --git a/plugins/AnonymousPiwikUsageMeasurement/config/test.php b/plugins/AnonymousPiwikUsageMeasurement/config/test.php new file mode 100644 index 0000000000000000000000000000000000000000..09e7a71cc5d937a7fca1b69f05495640d96b6bb4 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/config/test.php @@ -0,0 +1,21 @@ +<?php + +return array( + 'AnonymousPiwikUsageMeasurement.piwikVersion' => '2.14.3', + 'AnonymousPiwikUsageMeasurement.phpVersion' => '5.5.27', + 'Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings' => Piwik\DI::factory(function () { + // we cannot decorate here as we need to create an instance of settings as super user, the permissions + // for writing / reading are detected on settings creation, not each time it is executed + + $settings = null; + \Piwik\Access::doAsSuperUser(function () use (&$settings) { + $settings = new Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings(); + // make sure no tracking is enabled when running tests, especially on travis and during ui tests + $settings->ownPiwikSiteId->setValue(0); + $settings->customPiwikSiteId->setValue(0); + }); + + return $settings; + }), + +); diff --git a/plugins/AnonymousPiwikUsageMeasurement/docs/faq.md b/plugins/AnonymousPiwikUsageMeasurement/docs/faq.md new file mode 100644 index 0000000000000000000000000000000000000000..c5aa4be47f303a43c1f20c8bb63d35e187dad4cd --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/docs/faq.md @@ -0,0 +1,10 @@ +## FAQ + +__Are there any prerequisites?__ + +* If tracking to a custom Matomo installation is enabled, your Matomo installation and your Matomo users must be able to connect to this instance +* If tracking to a custom Matomo installation is enabled and your Matomo is served via HTTPS, the custom Matomo installation must be available via HTTPS as well + +__Why was this plugin created?__ + +This plugin was created to provide a simple way to measure how Matomo product itself is being used. diff --git a/plugins/AnonymousPiwikUsageMeasurement/javascripts/tracking.js b/plugins/AnonymousPiwikUsageMeasurement/javascripts/tracking.js new file mode 100644 index 0000000000000000000000000000000000000000..8a3c3ad896cce564cae49872ab210054043e7c29 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/javascripts/tracking.js @@ -0,0 +1,163 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +if ('undefined' === (typeof piwikUsageTracking) || !piwikUsageTracking) { + piwikUsageTracking = {targets: [], visitorCustomVariables: [], trackingDomain: '', exampleDomain: ''}; +} + +piwikUsageTracking.trackers = []; +piwikUsageTracking.initialized = false; +piwikUsageTracking.createTrackersIfNeeded = function () +{ + if (piwikUsageTracking.initialized) { + return; + } + + piwikUsageTracking.trackers = []; + piwikUsageTracking.initialized = true; + + if (!piwikUsageTracking.targets || !piwikUsageTracking.targets.length) { + return; + } + + var isHttps = location.protocol == "https:"; + + $.each(piwikUsageTracking.targets, function (index, target) { + if ('undefined' === (typeof Piwik)) { + // blocked by ad blocker + return; + } + + if (isHttps && 0 === target.url.indexOf('http:')) { + target.url = 'https://' + target.url.substring(location.protocol.length); + } + + var tracker = Piwik.getTracker(target.url, target.idSite); + tracker.isAnon = target.useAnonymization; + // we could do those calls later via `paq.push` but I want to make sure those methods are called and anonymized + // eg if there was a typo in `setDocumentTitle` we would not notice the method is not executed otherwise and + // it would result in a not anonymized title + + if (!target.useAnonymization) { + tracker.setUserId(piwikUsageTracking.userId); + } + + anonymizeReferrer(tracker); + anonymizeTitle(tracker); + anonymizeUrl(tracker); + + + piwikUsageTracking.trackers.push(tracker); + }); + + // we cannot enable link tracking or JS error tracking as it is too likely that custom data is tracked + $.each(piwikUsageTracking.visitorCustomVariables, function (j, customVar) { + _paq.push(['setCustomVariable', customVar.id, customVar.name, customVar.value]); + }); + + trackPageView(); + + function ucfirst(text) + { + var firstLetter = ('' + text).charAt(0).toUpperCase(); + return firstLetter + text.substr(1); + } + + function createUrlAnonymizer() + { + return new UrlAnonymizer(location.href); + } + + function anonymizeTitle(tracker) + { + if (!tracker.isAnon) return; + + var urlAnonymizer = createUrlAnonymizer(); + var module = urlAnonymizer.getTopLevelId() + var action = urlAnonymizer.getSubLevelId(); + + var title = 'Matomo Web Analytics'; + + if (module) { + title = ucfirst(module); + + if (action) { + title += ' ' + ucfirst(action); + } + } else if ($('#login_form').length) { + // this is the case when eg opening http://example.matomo.org/ and one is not logged in (we show login page) + title = 'Login'; + } + + tracker.setDocumentTitle(title); + } + + function anonymizeReferrer(tracker) + { + if (!tracker.isAnon) return; + + tracker.setReferrerUrl(''); + } + + function anonymizeUrl(tracker) + { + if (!tracker.isAnon) return; + + var urlAnonymizer = createUrlAnonymizer(); + var url = urlAnonymizer.getAnonymizedUrl(); + url = urlAnonymizer.makeUrlHierarchical(url); + + tracker.setCustomUrl(url); + } + + function trackPageView() + { + var urlAnonymizer = createUrlAnonymizer(); + var popover = urlAnonymizer.getPopoverNameFromUrl(); + + (piwikUsageTracking.trackers || []).forEach(anonymizeTitle); + (piwikUsageTracking.trackers || []).forEach(anonymizeUrl); + + if (popover) { + _paq.push(['setCustomVariable', 1, 'popover', popover, 'page']); + } + + _paq.push(['trackPageView']); + } + + $(function () { + window.CoreHome.Matomo.on('matomoPageChange', function () { + trackPageView(); + }); + }); +}; + +var _paq = { + push: function() { + piwikUsageTracking.createTrackersIfNeeded(); + + // push a method to all available trackers + $.each(arguments, function (i, parameterArray) { + var method = parameterArray.shift(); + + $.each(piwikUsageTracking.trackers, function (index, tracker) { + // the users piwik might not support a method yet, only execute if it exists + if (typeof method === 'string' && tracker[method]) { + tracker[method].apply(tracker, parameterArray); + } else if (typeof method === 'function') { + method.apply(tracker, parameterArray); + } + }); + }); + } +}; + +window.addEventListener('DOMContentLoaded', function () { + if (piwikUsageTracking && piwikUsageTracking.createTrackersIfNeeded) { + piwikUsageTracking.createTrackersIfNeeded(); + } +}); diff --git a/plugins/AnonymousPiwikUsageMeasurement/javascripts/tracking.spec.js b/plugins/AnonymousPiwikUsageMeasurement/javascripts/tracking.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7f699ad220b4dc7f726c9e0ff15684fd5587c506 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/javascripts/tracking.spec.js @@ -0,0 +1,135 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +(function () { + + function initAnonymizedTrackers() { + "use strict"; + initTrackers(true); + } + + function initNotAnonymizedTrackers() { + "use strict"; + initTrackers(false); + } + + function initTrackers(anonymize) { + "use strict"; + + piwikUsageTracking.userId = "testuserid"; + + for (var i = 0; i < piwikUsageTracking.targets.length; i++) { + piwikUsageTracking.targets[i].useAnonymization = anonymize; + } + + piwikUsageTracking.initialized = false; + piwikUsageTracking.createTrackersIfNeeded(); + } + + beforeEach(initAnonymizedTrackers); + + describe('AnonymousPiwikUsageMeasurementTracking', function() { + var anonymousDomain = 'http://anonymous.piwikdomain.org'; + var trackingDomain = piwikUsageTracking.trackingDomain; + + describe('#initialized', function() { + it('should be initialized automatically', function() { + expect(piwikUsageTracking.initialized).to.be.true; + expect(piwikUsageTracking.trackers.length).to.eql(3); + }); + + it('should be initialized by createTrackers method if not initialized yet', function() { + piwikUsageTracking.initialized = false; + piwikUsageTracking.createTrackersIfNeeded(); + expect(piwikUsageTracking.initialized).to.be.true; + expect(piwikUsageTracking.trackers.length).to.eql(3); + }); + + it('should be initialized by a push method', function() { + piwikUsageTracking.initialized = false; + _paq.push(['getTrackerUrl']); + expect(piwikUsageTracking.initialized).to.be.true; + expect(piwikUsageTracking.trackers.length).to.eql(3); + }); + }); + + + describe('#trackers', function() { + it('should have created multiple trackers with correct idSite, domain and custom variables', function() { + var tracker1 = piwikUsageTracking.trackers[0]; + var tracker2 = piwikUsageTracking.trackers[1]; + var tracker3 = piwikUsageTracking.trackers[2]; + + expect(tracker1.getTrackerUrl()).to.eql('http://localhost/matomo.php'); + expect(tracker1.getSiteId()).to.eql(1); + expect(tracker1.getCustomVariable(1, 'visit')).to.eql(['Access', 'user']); + + expect(tracker2.getTrackerUrl()).to.eql('http://127.0.0.1/matomo.php'); + expect(tracker2.getSiteId()).to.eql(20); + expect(tracker2.getCustomVariable(1, 'visit')).to.eql(['Access', 'user']); + + expect(tracker3.getTrackerUrl()).to.eql('http://localhost/matomo.php'); + expect(tracker3.getSiteId()).to.eql(33); + expect(tracker3.getCustomVariable(1, 'visit')).to.eql(['Access', 'user']); + }); + + it('should anonymize url and referrer by default', function() { + + for (var i = 1; i < piwikUsageTracking.trackers.length; i++) { + var tracker = piwikUsageTracking.trackers[i]; + + var request = tracker.getRequest(''); + expect(request).to.contain('&url=http%3A%2F%2Fdemo.matomo.org%2F%3F'); + expect(request).to.contain('&_cvar=%7B%221%22%3A%5B%22Access%22%2C%22user%22%5D%7D'); + expect(request).to.not.contain('urlref='); + expect(request).to.not.contain('uid=testuserid'); + } + + }); + + it('should not anonymize url and referrer', function() { + + initNotAnonymizedTrackers(); + + for (var i = 1; i < piwikUsageTracking.trackers.length; i++) { + var tracker = piwikUsageTracking.trackers[i]; + + var request = tracker.getRequest(''); + expect(request).to.not.contain('&url=http%3A%2F%2Fdemo.matomo.org%2F%3F'); + expect(request).to.contain('urlref='); + expect(request).to.contain('uid=testuserid'); + + } + + }); + + }); + + describe('_paq.push', function() { + it('should call a method with parameters on all tracker instances', function() { + var tracker1 = piwikUsageTracking.trackers[0]; + var tracker2 = piwikUsageTracking.trackers[1]; + var tracker3 = piwikUsageTracking.trackers[2]; + + _paq.push(['setCustomData', 'mykey', 'myvalue']); + + expect(tracker1.getCustomData()).to.eql({mykey: 'myvalue'}); + expect(tracker2.getCustomData()).to.eql({mykey: 'myvalue'}); + expect(tracker3.getCustomData()).to.eql({mykey: 'myvalue'}); + }); + + it('should not fail when calling a non existing tracker method', function() { + _paq.push(['setNotExisTiNGmeThod', 'mykey', 'myvalue']); + }); + + it('should call a function in the context of tracker', function() { + _paq.push([function () { + expect(this.getSiteId()).to.be.at.least(1); + }]); + }); + }); + }); +})(); diff --git a/plugins/AnonymousPiwikUsageMeasurement/javascripts/url.js b/plugins/AnonymousPiwikUsageMeasurement/javascripts/url.js new file mode 100644 index 0000000000000000000000000000000000000000..dc001d30bfa75678a3a2e614748617e21858c41e --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/javascripts/url.js @@ -0,0 +1,324 @@ +var UrlAnonymizer = function (absoluteUrl) { + + var whitelistUrlParams = ['module', 'action', 'idSite', 'idDashboard', 'period', 'date', 'popover', 'idGoal', 'pluginName', 'category', 'subcategory']; + var isPiwik3Reporting = isPiwik3ReportingUrl(); + + function isPiwik3ReportingUrl() + { + var category = broadcast.getValueFromHash('category', absoluteUrl); + var subcategory = broadcast.getValueFromHash('subcategory', absoluteUrl); + + if (!!category && !!subcategory) { + return true; + } + + category = broadcast.getValueFromUrl('category', absoluteUrl); + subcategory = broadcast.getValueFromUrl('subcategory', absoluteUrl); + + return (!!category && !!subcategory); + } + + function getTrackingDomain() + { + if ('undefined' !== (typeof piwikUsageTracking) + && piwikUsageTracking && piwikUsageTracking.trackingDomain) { + return piwikUsageTracking.trackingDomain; + } + } + + function removeFirstPartFromTranslationKeyIfPresent(translationKey) + { + var posUnderscore = translationKey.indexOf('_'); + if (posUnderscore >= 1) { + translationKey = translationKey.substr(posUnderscore + 1); + } + + return translationKey; + } + + function isNumber(string) + { + return /^\d+$/.test(string + ''); + } + + function getTopLevelId() + { + var id = ''; + if (isPiwik3Reporting) { + id = getValueFromHashOrUrl('category', absoluteUrl); + id = removeFirstPartFromTranslationKeyIfPresent(id); + } else { + id = getValueFromHashOrUrl('module'); + } + + return id; + } + + function getSubLevelId() + { + var id = ''; + + if (isPiwik3Reporting) { + id = broadcast.getValueFromHash('subcategory', absoluteUrl); + id = removeFirstPartFromTranslationKeyIfPresent(id); + + if (isNumber(id)) { + // prevent titles or urls like 'Dashboard 6' (Dashboard Id 6) or 'Goal 5' (Goal Id 5) + id = ''; + } + } else { + id = getValueFromHashOrUrl('action'); + } + + return id; + } + + function makeUrlHierarchical(url) + { + var anonymizer = new UrlAnonymizer(url); + + var module = anonymizer.getTopLevelId(); + var action = anonymizer.getSubLevelId(); + + if (!action) { + action = 'default'; + } + + var search = anonymizer.getSearchFromUrl(); + + var hierarchicalUrl = getTrackingDomain() + '/'; + if (module) { + hierarchicalUrl += module + '/' + action + '/'; + } + + var searchParams = broadcast.extractKeyValuePairsFromQueryString(search); + + delete searchParams['module']; + delete searchParams['action']; + delete searchParams['category']; + delete searchParams['subcategory']; + + hierarchicalUrl += '?'; + $.each(searchParams, function (key, value) { + hierarchicalUrl += key + '=' + value + '&'; + }); + + if (endsWith(hierarchicalUrl, '&')) { + hierarchicalUrl = hierarchicalUrl.substr(0, hierarchicalUrl.length - 1); + } + + return hierarchicalUrl; + } + + function endsWith(string, needle) + { + return string.substr(-1 * needle.length, needle.length) === needle; + } + + function getSearchFromUrl() + { + var searchPos = absoluteUrl.indexOf('?'); + + if (-1 === searchPos) { + return ''; + } + + var search = absoluteUrl.substr(searchPos + 1); + + if (search && search.match('#')) { + search = search.substring(0, search.indexOf("#")); + } + + return search; + } + + function getBlacklistedUrlParams() + { + var search = getSearchFromUrl(); + + if (!search) { + return []; + } + + var pairs = broadcast.extractKeyValuePairsFromQueryString(search); + var blacklisted = []; + $.each(pairs, function (key, value) { + if (-1 === whitelistUrlParams.indexOf(key)) { + blacklisted.push(key); + } + }); + + return blacklisted; + } + + function anonymizeUrlParams(paramsToAnonymize) + { + $.each(paramsToAnonymize, function (i, paramToAnonymize) { + if (-1 !== absoluteUrl.indexOf(paramToAnonymize + '=')) { + // make sure to anonymize token_auth + absoluteUrl = broadcast.updateParamValue(paramToAnonymize + '=XYZ', absoluteUrl); + + var toBeReplaced = paramToAnonymize + '=&' + paramToAnonymize + '=XYZ'; + if (absoluteUrl.indexOf(toBeReplaced) > 0) { + // workaround a bug in broadcast.updateParamValue when paramToAnonymize is given but no value assigned + absoluteUrl = absoluteUrl.replace(toBeReplaced, paramToAnonymize + '=XYZ'); + } + } + }); + + return absoluteUrl; + } + + function anonymizeIntegerValueInUrl(urlParam) + { + if (-1 !== absoluteUrl.indexOf(urlParam + '=')) { + var oldValue = getValueFromHashOrUrl(urlParam) + var newValue = (oldValue % 20); // max 20 + if (newValue === 0) { + newValue = 20; + } + // anonymize idSite, if there is a Piwik instance having many sites, one could maybe identify a specific + // instance based on a high siteid + absoluteUrl = broadcast.updateParamValue(urlParam + '=' + newValue, absoluteUrl); + } + + return absoluteUrl; + } + + function anonymizePopoverName() + { + var popoverName = getPopoverNameFromUrl(); + if (popoverName) { + // anonymize visitor ids etc. Otherwise one could check if your own visitor id is present + // and identify specific piwik instances and see which Piwik and PHP version they use, whether they use + // this plugin and what they do with their piwik. Also a transition or row evolution could include a URL + // etc + absoluteUrl = broadcast.updateParamValue('popover=' + popoverName, absoluteUrl); + } else if (-1 !== absoluteUrl.indexOf('popover=')) { + absoluteUrl = anonymizeUrlParams(['popover']); + } + + return absoluteUrl; + } + + function getHashFromUrl() + { + if (absoluteUrl && absoluteUrl.match('#')) { + return absoluteUrl.substring(absoluteUrl.indexOf("#"), absoluteUrl.length); + } + + return ''; + } + + function getAnonymizedUrl() + { + var anonymizedUrl; + var hash = getHashFromUrl(); + var search = getSearchFromUrl(); + + if (hash && hash.length > 4) { + anonymizedUrl = makeDemoPiwikUrl(hash); + } else if (search && search.length > 4) { + anonymizedUrl = makeDemoPiwikUrl(search); + } else { + anonymizedUrl = makeDemoPiwikUrl(''); + } + + var anonmized = new UrlAnonymizer(anonymizedUrl); + + var blacklistedParams = anonmized.getBlacklistedUrlParams(); + + blacklistedParams.push('token_auth'); + + // make sure to anonymize nonce, otherwise one could look for nonce token on tracked matomo and test it + // on several instances + blacklistedParams.push('nonce'); + + anonmized.anonymizeUrlParams(blacklistedParams); + + // anonymize idGoal, if there is a Piwik instance having many goal ids and the goal is tracked manually, + // one could maybe identify a specific instance + anonmized.anonymizeIntegerValueInUrl('idGoal'); + // anonymize idSite, if there is a Piwik instance having many sites, one could maybe identify a specific + // instance based on a high siteid + anonmized.anonymizeIntegerValueInUrl('idSite'); + anonmized.anonymizeIntegerValueInUrl('idDashboard'); + + anonymizedUrl = anonmized.anonymizePopoverName(); + + return anonymizedUrl; + } + + function makeDemoPiwikUrl(url) + { + if (0 === url.indexOf('#')) { + url = url.substr(1); + } + + if (0 === url.indexOf('?')) { + url = url.substr(1); + } + + return getTrackingDomain() + '/index.php?' + url; + } + + function getValueFromHashOrUrl(param) + { + var isHashUrlInPiwik3 = isPiwik3Reporting && broadcast.getValueFromHash('category', absoluteUrl); + var isHashUrlInPiwik2 = !isPiwik3Reporting && broadcast.getValueFromHash('module', absoluteUrl); + + if (isHashUrlInPiwik3 || isHashUrlInPiwik2) { + + return broadcast.getValueFromHash(param, absoluteUrl); + // fallback to Url only if there is no url in hash specified. Otherwise we'd return wrong value, + // eg action doesn't have to be specified in hash, using the one from Url would be wrong if there is a module + // specified in hash + } + + var value = broadcast.getValueFromUrl(param, absoluteUrl); + + if (!value) { + // we make sure to work with strings + value = ''; + } + + return value; + } + + function getPopoverNameFromUrl() + { + var popover = getValueFromHashOrUrl('popover'); + + if (popover && -1 !== popover.indexOf('$')) { + popover = decodeURIComponent(popover.replace(/\$/g, '%')); + + if (0 === popover.indexOf('RowAction:')) { + popover = popover.split(':', 2).join(':'); + // use max first two parts as a URL includes a ":" as well eg "RowAction:Transitions:url:http://" + // to be safe we use only "RowAction:Transitions" in this case. + return popover; + } else if (-1 !== popover.indexOf(':')) { + // eg "visitorProfile:7c0811af3c64efab", we only want to keep "VisitorProfile" + return popover.substr(0, popover.indexOf(':')); + } + } + + return ''; + } + + return { + makeUrlHierarchical: makeUrlHierarchical, + isPiwik3ReportingUrl: isPiwik3ReportingUrl, + getAnonymizedUrl: getAnonymizedUrl, + getValueFromHashOrUrl: getValueFromHashOrUrl, + getPopoverNameFromUrl: getPopoverNameFromUrl, + getTopLevelId: getTopLevelId, + getSubLevelId: getSubLevelId, + getBlacklistedUrlParams: getBlacklistedUrlParams, + getSearchFromUrl: getSearchFromUrl, + getHashFromUrl: getHashFromUrl, + anonymizeIntegerValueInUrl: anonymizeIntegerValueInUrl, + anonymizeUrlParams: anonymizeUrlParams, + anonymizePopoverName: anonymizePopoverName + }; +}; \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/javascripts/url.spec.js b/plugins/AnonymousPiwikUsageMeasurement/javascripts/url.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..858006bc16d2c8e65fa6a3631bd56efd296b27d2 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/javascripts/url.spec.js @@ -0,0 +1,460 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +(function () { + describe('AnonymousPiwikUsageMeasurementUrl', function() { + var anonymousDomain = 'http://anonymous.piwikdomain.org'; + var trackingDomain = piwikUsageTracking.trackingDomain; + + function buildUrl(search, hash) + { + return anonymousDomain + "/index.php" + search + hash; + } + + var fakeSearch = "?module=CoreHome&action=index&idSite=43&period=day&date=yesterday"; + var fakeHash = "#?module=Dashboard&action=embeddedIndex&idSite=43&period=day&date=yesterday&idDashboard=25"; + var piwik3Hash = "#?idSite=1&period=day&date=yesterday&category=General_Actions&subcategory=Events_Events"; + + var fakeUrl = buildUrl(fakeSearch, fakeHash); + var piwik3Url = buildUrl(fakeSearch, piwik3Hash); + + describe('#makeUrlHierarchical', function() { + var urlAnonymizer = new UrlAnonymizer(''); + + it('should be a function', function() { + expect(UrlAnonymizer).to.be.a('function'); + }); + + it('should make an url hierarchical', function() { + var url = urlAnonymizer.makeUrlHierarchical(anonymousDomain + '/index.php?module=CoreHome&action=myAction&idSite=3&date=today&period=day') + expect(url).to.eql(trackingDomain + '/CoreHome/myAction/?idSite=3&date=today&period=day'); + }); + + it('should not make an url hierarchical if no module is given', function() { + var url = urlAnonymizer.makeUrlHierarchical(anonymousDomain + '/index.php?idSite=3&date=today&period=day') + expect(url).to.eql(trackingDomain + '/?idSite=3&date=today&period=day'); + }); + + it('should use a default action when making url hierarchical', function() { + var url = urlAnonymizer.makeUrlHierarchical(anonymousDomain + '/index.php?module=CoreHome&idSite=3&date=today&period=day') + expect(url).to.eql(trackingDomain + '/CoreHome/default/?idSite=3&date=today&period=day'); + }); + + it('should correctly change a matomo 3 url', function() { + var url = urlAnonymizer.makeUrlHierarchical(anonymousDomain + '/index.php?idSite=1&period=day#?idSite=1&period=day&date=yesterday&category=General_Actions&subcategory=Events_Events') + expect(url).to.eql(trackingDomain + '/Actions/Events/?idSite=1&period=day'); + }); + + it('should ignore sublevel id in case it is a number', function() { + var url = urlAnonymizer.makeUrlHierarchical(anonymousDomain + '/index.php?idSite=1&period=day&date=yesterday&category=Dashboard_Dashboard&subcategory=5') + expect(url).to.eql(trackingDomain + '/Dashboard/default/?idSite=1&period=day&date=yesterday'); + }); + }); + + describe('#getAnonymizedUrl', function() { + + it('should anonymize url, hash given, should ignore search', function () { + var urlAnonymizer = new UrlAnonymizer(fakeUrl); + var url = urlAnonymizer.getAnonymizedUrl(); + expect(url).to.eql(trackingDomain + "/index.php?module=Dashboard&action=embeddedIndex&idSite=3&period=day&date=yesterday&idDashboard=5"); + }); + + it('should anonymize url, hash not given, should use search', function () { + var urlAnonymizer = new UrlAnonymizer(fakeSearch, ""); + var url = urlAnonymizer.getAnonymizedUrl(); + expect(url).to.eql(trackingDomain + "/index.php?module=CoreHome&action=index&idSite=3&period=day&date=yesterday"); + }); + + it('should anonymize url, hash and search not given', function () { + + var urlAnonymizer = new UrlAnonymizer("", ""); + var url = urlAnonymizer.getAnonymizedUrl(); + expect(url).to.eql(trackingDomain + "/index.php?"); + }); + + it('should anonymize idGoal, idDashboard, tokenAuth, nonce, and all not whitelisted params', function () { + + var hash = fakeHash + '&token_auth=c4ca4238a0b923820dcc509a6f75849b&nonce=d4ca4238a0b923820dcc509a6f75849b&pluginName=KeepThisOne&test=1&anyOther=4&idGoal=343¬allowed='; + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, hash)); + var url = urlAnonymizer.getAnonymizedUrl(); + + expect(url).to.eql(trackingDomain + "/index.php?module=Dashboard&action=embeddedIndex&idSite=3&period=day&date=yesterday&idDashboard=5&token_auth=XYZ&nonce=XYZ&pluginName=KeepThisOne&test=XYZ&anyOther=XYZ&idGoal=3¬allowed=XYZ"); + }); + + it('should anonymize popover', function () { + + var hash = '#?popover=browsePluginDetail$3ABotTracker'; + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, hash)); + var url = urlAnonymizer.getAnonymizedUrl(); + expect(url).to.eql(trackingDomain + "/index.php?popover=browsePluginDetail"); + }); + + it('should anonymize rowaction popover', function () { + + var hash = "#?module=Dashboard&action=embeddedIndex&idSite=1&popover=RowAction$3ARowEvolution$3AActions.getPageUrls$3A$257B$257D$3Adocs"; + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, hash)); + var url = urlAnonymizer.getAnonymizedUrl(); + + expect(url).to.eql(trackingDomain + "/index.php?module=Dashboard&action=embeddedIndex&idSite=1&popover=RowAction:RowEvolution"); + }); + }); + + describe('#getBlacklistedUrlParams', function() { + + it('should anonymize url, hash given, should ignore search', function() { + var urlAnonymizer = new UrlAnonymizer(fakeUrl); + var url = urlAnonymizer.getBlacklistedUrlParams(); + expect(url).to.eql([]); + }); + + it('should anonymize url, hash not given, should use search', function() { + var urlAnonymizer = new UrlAnonymizer(fakeSearch, ""); + var url = urlAnonymizer.getBlacklistedUrlParams(); + expect(url).to.eql([]); + }); + + it('should anonymize url, hash and search not given', function() { + + var urlAnonymizer = new UrlAnonymizer("", ""); + var url = urlAnonymizer.getBlacklistedUrlParams(); + expect(url).to.eql([]); + }); + + it('should find not whitelisted url params', function() { + + var search = fakeSearch + '&token_auth=c4ca4238a0b923820dcc509a6f75849b&nonce=d4ca4238a0b923820dcc509a6f75849b&pluginName=KeepThisOne&test=1&anyOther=4&idGoal=343¬allowed='; + var urlAnonymizer = new UrlAnonymizer(buildUrl(search, fakeHash)); + var url = urlAnonymizer.getBlacklistedUrlParams(); + + expect(url).to.eql(['token_auth', 'nonce', 'test', 'anyOther', 'notallowed']); + }); + + it('should not look in hash for url params', function() { + + var hash = fakeHash + '&token_auth=c4ca4238a0b923820dcc509a6f75849b&nonce=d4ca4238a0b923820dcc509a6f75849b&pluginName=KeepThisOne&test=1&anyOther=4&idGoal=343¬allowed='; + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, hash)); + var url = urlAnonymizer.getBlacklistedUrlParams(); + + expect(url).to.eql([]); + }); + + }); + + describe('#anonymizeIntegerValueInUrl', function() { + var baseUrl = anonymousDomain + "/index.php?module=CoreHome&action=index&idSite="; + + it('should not change url parameter when anonymizing is not needed as it is lower than limit', function () { + + var urlAnonymizer = new UrlAnonymizer(baseUrl + "1"); + var value = urlAnonymizer.anonymizeIntegerValueInUrl('idSite'); + expect(value).to.eql(baseUrl + "1"); + + urlAnonymizer = new UrlAnonymizer(baseUrl + "20"); + value = urlAnonymizer.anonymizeIntegerValueInUrl('idSite'); + expect(value).to.eql(baseUrl + "20"); + }); + + it('should anonymize url parameter when integer is higher than limit', function () { + + var urlAnonymizer = new UrlAnonymizer(baseUrl + "21"); + var value = urlAnonymizer.anonymizeIntegerValueInUrl('idSite'); + expect(value).to.eql(baseUrl + "1"); + + urlAnonymizer = new UrlAnonymizer(baseUrl + "23"); + value = urlAnonymizer.anonymizeIntegerValueInUrl('idSite'); + expect(value).to.eql(baseUrl + "3"); + + urlAnonymizer = new UrlAnonymizer(baseUrl + "0"); + value = urlAnonymizer.anonymizeIntegerValueInUrl('idSite'); + expect(value).to.eql(baseUrl + "20"); + }); + + it('should convert a text to NaN', function () { + var urlAnonymizer = new UrlAnonymizer(baseUrl + "text"); + var value = urlAnonymizer.anonymizeIntegerValueInUrl('idSite'); + expect(value).to.eql(baseUrl + "NaN"); + }); + }); + + describe('#getValueFromHashOrUrl', function() { + + it('should prefer value from hash when module is given in hash', function() { + + var url = anonymousDomain + "/index.php?module=CoreHome&action=index&idSite=1&period=day&date=yesterday&activated=&mode=user#?module=Actions&action=menuGetPageUrls&idSite=1&period=day&date=yesterday&mode=user"; + + var urlAnonymizer = new UrlAnonymizer(url); + var value = urlAnonymizer.getValueFromHashOrUrl('module'); + expect(value).to.eql("Actions"); + + value = urlAnonymizer.getValueFromHashOrUrl('action'); + expect(value).to.eql("menuGetPageUrls"); + }); + + it('should not read value from search when module is given in hash', function() { + + var url = anonymousDomain + "/index.php?module=CoreHome&action=index&idSite=1&period=day&date=yesterday&activated=&mode=user#?module=Actions"; + + var urlAnonymizer = new UrlAnonymizer(url); + var value = urlAnonymizer.getValueFromHashOrUrl('module'); + expect(value).to.eql("Actions"); + value = urlAnonymizer.getValueFromHashOrUrl('action'); + expect(value).to.eql(""); + value = urlAnonymizer.getValueFromHashOrUrl('idSite'); + expect(value).to.eql(""); + }); + + it('should read value from search when no module is given in hash', function() { + + var url = anonymousDomain + "/index.php?module=CoreHome&action=index&idSite=1&period=day&date=yesterday&activated=&mode=user#?action=menuGetPageUrls"; + + var urlAnonymizer = new UrlAnonymizer(url); + var value = urlAnonymizer.getValueFromHashOrUrl('module'); + expect(value).to.eql("CoreHome"); + value = urlAnonymizer.getValueFromHashOrUrl('action'); + expect(value).to.eql("index"); + value = urlAnonymizer.getValueFromHashOrUrl('idSite'); + expect(value).to.eql("1"); + }); + }); + + describe('#getPopoverNameFromUrl', function() { + + it('should read anonymized popover name from search', function() { + var url = anonymousDomain + "/index.php?test=search&popover=browsePluginDetail$3ABotTracker"; + + var urlAnonymizer = new UrlAnonymizer(url); + var popoverName = urlAnonymizer.getPopoverNameFromUrl(); + expect(popoverName).to.eql("browsePluginDetail"); + }); + + it('should read anonymized popover name from hash', function() { + var url = anonymousDomain + "/index.php?test=search#?module=CoreHome&popover=browsePluginDetail$3ABotTracker"; + + var urlAnonymizer = new UrlAnonymizer(url); + var popoverName = urlAnonymizer.getPopoverNameFromUrl(); + expect(popoverName).to.eql("browsePluginDetail"); + }); + + it('should read anonymized rowaction popover name from search', function() { + var url = anonymousDomain + "/index.php?test=search&popover=RowAction$3ATransitions$3Aurl$3Ahttp$3A$2F$2Fpiwik.org$2F"; + + var urlAnonymizer = new UrlAnonymizer(url); + var popoverName = urlAnonymizer.getPopoverNameFromUrl(); + expect(popoverName).to.eql("RowAction:Transitions"); + }); + + it('should read anonymized rowaction popover name from hash', function() { + var url = anonymousDomain + "/index.php?test=search#?module=CoreHome&popover=RowAction$3ATransitions$3Aurl$3Ahttp$3A$2F$2Fpiwik.org$2F"; + + var urlAnonymizer = new UrlAnonymizer(url); + var popoverName = urlAnonymizer.getPopoverNameFromUrl(); + expect(popoverName).to.eql("RowAction:Transitions"); + }); + }); + + describe('#anonymizePopoverName', function() { + var baseUrl = anonymousDomain + "/index.php?test=search&popover="; + + it('should anonymized popover name', function() { + + var urlAnonymizer = new UrlAnonymizer(baseUrl + "browsePluginDetail$3ABotTracker"); + var popoverName = urlAnonymizer.anonymizePopoverName(); + expect(popoverName).to.eql(baseUrl + "browsePluginDetail"); + }); + + it('should anonymize rowaction popover name', function() { + + var urlAnonymizer = new UrlAnonymizer(baseUrl + "RowAction$3ATransitions$3Aurl$3Ahttp$3A$2F$2Fpiwik.org$2F"); + var popoverName = urlAnonymizer.anonymizePopoverName(); + expect(popoverName).to.eql(baseUrl + "RowAction:Transitions"); + }); + + it('should anonymize popover with hard coded value in case we cannot detect popover format to be sure it is anonymized', function() { + var urlAnonymizer = new UrlAnonymizer(baseUrl); + var popoverName = urlAnonymizer.anonymizePopoverName(); + expect(popoverName).to.eql(baseUrl + "XYZ"); + }); + + it('should not anonymize anything when no popover param is given', function() { + var urlAnonymizer = new UrlAnonymizer(anonymousDomain + "/index.php?test=search"); + var popoverName = urlAnonymizer.anonymizePopoverName(); + expect(popoverName).to.eql(anonymousDomain + "/index.php?test=search"); + }); + }); + + describe('#anonymizeUrlParams', function() { + var baseUrl = anonymousDomain + "/index.php?test=search&token_auth="; + + it('should not anonymize anything when no params given', function() { + + var urlAnonymizer = new UrlAnonymizer(baseUrl); + var url = urlAnonymizer.anonymizeUrlParams([]); + expect(url).to.eql(baseUrl); + }); + + it('should anonymize given url param', function() { + + var urlAnonymizer = new UrlAnonymizer(baseUrl + "123485689453453455fdf334"); + var url = urlAnonymizer.anonymizeUrlParams(['token_auth']); + expect(url).to.eql(baseUrl + "XYZ"); + }); + + it('should not anonymize anything when url parameter is not given', function() { + + var urlAnonymizer = new UrlAnonymizer(baseUrl); + var url = urlAnonymizer.anonymizeUrlParams(['foobarnotexist']); + expect(url).to.eql(baseUrl); + }); + + it('should anonymize when value is empty', function() { + var urlAnonymizer = new UrlAnonymizer(baseUrl); + var url = urlAnonymizer.anonymizeUrlParams(['token_auth']); + expect(url).to.eql(baseUrl + "XYZ"); + }); + + it('should be able to anonymize many url parameters', function() { + var urlAnonymizer = new UrlAnonymizer(baseUrl + "123485689453453455fdf334"); + var url = urlAnonymizer.anonymizeUrlParams(['token_auth', 'test', 'foobarnotexist']); + expect(url).to.eql(anonymousDomain + "/index.php?test=XYZ&token_auth=XYZ"); + }); + }); + + describe('#getHashFromUrl', function() { + + it('should detect hash correctly', function() { + var urlAnonymizer = new UrlAnonymizer(fakeUrl); + var hash = urlAnonymizer.getHashFromUrl(); + expect(hash).to.eql(fakeHash); + }); + + it('should return empy string if contains search but no hash', function() { + var urlAnonymizer = new UrlAnonymizer('http://example.org?query=foo'); + var hash = urlAnonymizer.getHashFromUrl(); + expect(hash).to.eql(''); + }); + + it('should return empy string if url does not contain hash or query', function() { + var urlAnonymizer = new UrlAnonymizer('http://example.org'); + var hash = urlAnonymizer.getHashFromUrl(); + expect(hash).to.eql(''); + }); + }); + + describe('#getSearchFromUrl', function() { + + it('should detect search correctly', function() { + var urlAnonymizer = new UrlAnonymizer(fakeUrl); + var search = urlAnonymizer.getSearchFromUrl(); + expect(search).to.eql(fakeSearch.substr(1)); + }); + + it('should return empy string if url does contain hash but no search', function() { + var urlAnonymizer = new UrlAnonymizer('http://example.org#foo'); + var search = urlAnonymizer.getSearchFromUrl(); + expect(search).to.eql(''); + }); + + it('should return empy string if url does not contain search', function() { + var urlAnonymizer = new UrlAnonymizer('http://example.org'); + var search = urlAnonymizer.getSearchFromUrl(); + expect(search).to.eql(''); + }); + }); + + describe('#isPiwik3ReportingUrl', function() { + + it('should not be if it contains module and action in hash but no category and subcategory', function() { + var urlAnonymizer = new UrlAnonymizer(fakeUrl); + var isPiwik3 = urlAnonymizer.isPiwik3ReportingUrl(); + expect(isPiwik3).to.be.false; + }); + + it('should not be if no hash is given', function() { + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, "")); + var isPiwik3 = urlAnonymizer.isPiwik3ReportingUrl(); + expect(isPiwik3).to.be.false; + }); + + it('should be true in case it contains a category and subcategory', function() { + var urlAnonymizer = new UrlAnonymizer(piwik3Url); + var isPiwik3 = urlAnonymizer.isPiwik3ReportingUrl(); + expect(isPiwik3).to.be.true; + }); + + it('should be true in case category is zero', function() { + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, "#?idSite=1&period=day&date=yesterday&&category=0&subcategory=0")); + var isPiwik3 = urlAnonymizer.isPiwik3ReportingUrl(); + expect(isPiwik3).to.be.true; + }); + }); + + describe('#getTopLevelId', function() { + + it('should detect id from Matomo 2 URL', function() { + var urlAnonymizer = new UrlAnonymizer(fakeUrl); + var id = urlAnonymizer.getTopLevelId(); + expect(id).to.eql('Dashboard'); + }); + + it('should detect id from Matomo 2 URL and use module from search when hash does not contain module', function() { + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, '')); + var id = urlAnonymizer.getTopLevelId(); + expect(id).to.eql('CoreHome'); + }); + + it('should detect id from Matomo 3 URL', function() { + var urlAnonymizer = new UrlAnonymizer(piwik3Url); + var id = urlAnonymizer.getTopLevelId(); + expect(id).to.eql('Actions'); + }); + + it('should detect top level id when category is zero', function() { + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, "#?idSite=1&period=day&date=yesterday&category=0&subcategory=0")); + var id = urlAnonymizer.getTopLevelId(); + expect(id).to.eql('0'); + }); + + it('should return empty string if it does not contan top level id', function() { + var urlAnonymizer = new UrlAnonymizer("http://example.com"); + var id = urlAnonymizer.getTopLevelId(); + expect(id).to.eql(''); + }); + }); + + describe('#getSubLevelId', function() { + + it('should detect id from Matomo 2 URL', function() { + var urlAnonymizer = new UrlAnonymizer(fakeUrl); + var id = urlAnonymizer.getSubLevelId(); + expect(id).to.eql('embeddedIndex'); + }); + + it('should detect id from Matomo 2 URL and use module from search when hash does not contain module', function() { + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, '')); + var id = urlAnonymizer.getSubLevelId(); + expect(id).to.eql('index'); + }); + + it('should detect id from Matomo 3 URL', function() { + var urlAnonymizer = new UrlAnonymizer(piwik3Url); + var id = urlAnonymizer.getSubLevelId(); + expect(id).to.eql('Events'); + }); + + it('should convert a number in subcategory to an empty string', function() { + var urlAnonymizer = new UrlAnonymizer(buildUrl(fakeSearch, "#?idSite=1&period=day&date=yesterday&category=0&subcategory=0")); + var id = urlAnonymizer.getSubLevelId(); + expect(id).to.eql(''); + }); + + it('should return empty string if it does not contain top level id', function() { + var urlAnonymizer = new UrlAnonymizer("http://example.com"); + var id = urlAnonymizer.getSubLevelId(); + expect(id).to.eql(''); + }); + }); + }); +})(); diff --git a/plugins/AnonymousPiwikUsageMeasurement/phpcs.xml b/plugins/AnonymousPiwikUsageMeasurement/phpcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..ca2d13a0b8725e18d409a77b3ba81cefebcfee72 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/phpcs.xml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<ruleset name="anonymousPiwikUsageMeasurement" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"> + + <description>Matomo Coding Standard for AnonymousPiwikUsageMeasurement plugin</description> + + <arg name="extensions" value="php" /> + + <file>.</file> + + <exclude-pattern>tests/javascript/*</exclude-pattern> + <exclude-pattern>*/vendor/*</exclude-pattern> + + <rule ref="Matomo"></rule> + + <rule ref="Generic.Files.LineLength"> + <properties> + <property name="lineLimit" value="250" /> + </properties> + <exclude-pattern>tests/*</exclude-pattern> + </rule> + + <rule ref="Squiz.Classes.ValidClassName.NotCamelCaps"> + <!-- Classnames for our update files don't match PascalCase, this can't be changed easily --> + <exclude-pattern>Updates/*</exclude-pattern> + </rule> + + <rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"> + <!-- Allow using method name without camel caps in tests as long as some methods are named test_* --> + <exclude-pattern>tests/*</exclude-pattern> + </rule> + + <rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses"> + <!-- Allow using multiple classes in one file for tests --> + <exclude-pattern>tests/*</exclude-pattern> + </rule> +</ruleset> \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/plugin.json b/plugins/AnonymousPiwikUsageMeasurement/plugin.json new file mode 100644 index 0000000000000000000000000000000000000000..fcaf9c7a30ddbd177d2b9154aedd9d131270f7a4 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/plugin.json @@ -0,0 +1,31 @@ +{ + "name": "AnonymousPiwikUsageMeasurement", + "version": "5.0.2", + "description": "Send anonymized usage data to your own Matomo instance or to any other Matomo", + "theme": false, + "keywords": [ + "usage", + "tracking", + "measurement", + "matomo", + "piwik" + ], + "license": "GPL v3+", + "homepage": "https:\/\/matomo.org", + "require": { + "matomo": ">=5.0.0-b1,<6.0.0-b1" + }, + "support": { + "issues": "https://github.com/matomo-org/plugin-AnonymousPiwikUsageMeasurement/issues", + "forum": "https://forum.matomo.org", + "source": "https://github.com/matomo-org/plugin-AnonymousPiwikUsageMeasurement" + }, + "authors": [ + { + "name": "Matomo", + "email": "hello@matomo.org", + "homepage": "https:\/\/matomo.org" + } + ], + "category": "integration" +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/pull_request_template.md b/plugins/AnonymousPiwikUsageMeasurement/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..211089bb4ec77dcc05db341c8d1be45bae811eea --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/pull_request_template.md @@ -0,0 +1,29 @@ +## Description +<!-- Provide a brief but clear description of the changes introduced by this PR. + Explain WHY the change was necessary and HOW it fixes the issue. + If applicable, mention any dependencies or related changes. --> + +## Issue No +<!-- Mention the issue number this PR addresses. Use GitHub keywords like: + - Fixes #123 (closes the issue when merged) + - Resolves #123 + - Related to #123 (if it doesn’t close the issue) + - JIRA ticket number, if no GitHub issue available +--> + +## Steps to Replicate the Issue +1. <!-- Step 1: Describe the action taken. --> +2. <!-- Step 2: What was the expected result? --> +3. <!-- Step 3: What was the actual result? --> + + + +## Checklist +- [✔/✖] Tested locally or on demo2/demo3? +- [✔/✖/NA] New test case added/updated? +- [✔/✖/NA] Are all newly added texts included via translation? +- [✔/✖/NA] Are text sanitized properly? (Eg use of v-text v/s v-html for vue) +- [✔/✖/NA] Version bumped? +- [✔/✖/NA] I have understood, reviewed, and tested all AI outputs before use +- [✔/✖/NA] All AI instructions respect security, IP, and privacy rules +- [✔/✖/NA] Documentation updated? diff --git a/plugins/AnonymousPiwikUsageMeasurement/screenshots/AdminSettings.png b/plugins/AnonymousPiwikUsageMeasurement/screenshots/AdminSettings.png new file mode 100644 index 0000000000000000000000000000000000000000..506f5f5624a3586f09db14498cd93a33089accb6 Binary files /dev/null and b/plugins/AnonymousPiwikUsageMeasurement/screenshots/AdminSettings.png differ diff --git a/plugins/AnonymousPiwikUsageMeasurement/screenshots/UserSettings.png b/plugins/AnonymousPiwikUsageMeasurement/screenshots/UserSettings.png new file mode 100644 index 0000000000000000000000000000000000000000..3258706297066f31e1939d2911aaeaddbbbd7fcc Binary files /dev/null and b/plugins/AnonymousPiwikUsageMeasurement/screenshots/UserSettings.png differ diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/Fixtures/SendSystemReportTaskFixture.php b/plugins/AnonymousPiwikUsageMeasurement/tests/Fixtures/SendSystemReportTaskFixture.php new file mode 100644 index 0000000000000000000000000000000000000000..e747a0a0cacbf505e3cefe0e5f0bdfd5767e3ef4 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/Fixtures/SendSystemReportTaskFixture.php @@ -0,0 +1,72 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Fixtures; + +use Piwik\API\Request; +use Piwik\Date; +use Piwik\Tests\Framework\Fixture; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tasks; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Profiles; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\CustomVariables; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Targets; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Trackers; + +/** + * Generates tracker testing data for our TasksTest + * + * This Simple fixture adds one website and tracks one visit with couple pageviews and an ecommerce conversion + */ +class SendSystemReportTaskFixture extends Fixture +{ + public $dateTime = '2013-01-23 01:23:45'; + public $idSite = 1; + + public function setUp(): void + { + $this->setUpWebsite(); + $this->executeSomeApiMethods(); + + Fixture::createSuperUser(true); + $settings = new SystemSettings(); + $settings->ownPiwikSiteId->setValue($this->idSite); + $targets = new Targets($settings); + $trackers = new Trackers($targets); + $customVars = new CustomVariables(); + + $task = new Tasks($trackers, $customVars, new Profiles()); + $task->sendSystemReport(); + } + + public function tearDown(): void + { + // empty + } + + private function setUpWebsite() + { + if (!self::siteCreated($this->idSite)) { + $idSite = self::createWebsite($this->dateTime, $ecommerce = 1); + $this->assertSame($this->idSite, $idSite); + } + } + + private function executeSomeApiMethods() + { + Request::processRequest('API.getPiwikVersion'); + Request::processRequest('API.getSettings'); + Request::processRequest('UsersManager.getUsers'); + Request::processRequest('API.getPiwikVersion'); + Request::processRequest('VisitsSummary.get', array('idSite' => 1, 'period' => 'year', 'date' => 'today')); + + $date = Date::factory('today')->toString(); + Request::processRequest('CoreAdminHome.invalidateArchivedReports', array('idSites' => '1', 'period' => 'year', 'dates' => $date, 'cascadeDown' => '1')); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/AnonymousPiwikUsageMeasurementTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/AnonymousPiwikUsageMeasurementTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a70243441a32511ef39fae74da4048aa19e10ec0 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/AnonymousPiwikUsageMeasurementTest.php @@ -0,0 +1,145 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Integration; + +use Piwik\API\Request; +use Piwik\Container\StaticContainer; +use Piwik\Piwik; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Profiles; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\UserSettings; +use Piwik\Tests\Framework\Mock\FakeAccess; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group AnonymousPiwikUsageMeasurement + * @group AnonymousPiwikUsageMeasurementTest + * @group Plugins + */ +class AnonymousPiwikUsageMeasurementTest extends IntegrationTestCase +{ + public function test_shouldTrackApiCall() + { + Request::processRequest('API.getPiwikVersion'); + Request::processRequest('API.getSettings'); + Request::processRequest('UsersManager.getUsers'); + Request::processRequest('API.getPiwikVersion'); + + $profiles = new Profiles(); + $pushedProfiles = $profiles->popAll(); + + foreach ($pushedProfiles as &$pushedProfile) { + $this->assertNotEmpty($pushedProfile['creation_date']); + unset($pushedProfile['creation_date']); + + $this->assertGreaterThanOrEqual(1, $pushedProfile['wall_time']); + unset($pushedProfile['wall_time']); + } + + $expected = array( + array ( + 'category' => 'API', + 'name' => 'API', + 'action' => 'API.getPiwikVersion', + 'count' => '2', + ), + array ( + 'category' => 'API', + 'name' => 'API', + 'action' => 'API.getSettings', + 'count' => '1', + ), + array ( + 'category' => 'API', + 'name' => 'UsersManager', + 'action' => 'UsersManager.getUsers', + 'count' => '1', + ) + ); + + $this->assertEquals($expected, $pushedProfiles); + } + + public function test_shouldNotAddTargetsOrCustomVariables_IfDisabledByUser() + { + $settings = $this->makeUserSettings(); + $settings->userTrackingEnabled->setValue(false); + + $out = ''; + Piwik::postEvent('Template.jsGlobalVariables', array(&$out)); + + self::assertStringContainsString('var piwikUsageTracking = {"targets":[],"visitorCustomVariables":[],', $out); + } + + public function test_shouldAddTrackingCallsWithTargetsAndCustomVariables_IfEnabledByUser() + { + $settings = $this->makeUserSettings(); + $settings->userTrackingEnabled->setValue(true); + + $systemSettings = $this->makePluginSettings(); + $systemSettings->customPiwikSiteId->setValue(1); + $systemSettings->customPiwikSiteUrl->setValue('https://demo-anonymous.matomo.org/piwik.php'); + + $out = ''; + Piwik::postEvent('Template.jsGlobalVariables', array(&$out)); + self::assertStringContainsString('var piwikUsageTracking = {"targets":[{"url":"https:\/\/demo-anonymous.matomo.org\/piwik.php","idSite":1,"useAnonymization":true,"token_auth":null}],"visitorCustomVariables":[{"id":1,"name":"Access","value":"superuser"}],"trackingDomain":"https:\/\/demo-anonymous.matomo.org","exampleDomain":"http:\/\/example.com","userId":"superUserLogin"}', $out); + } + + public function test_shouldAlwaysAddTrackingCallAndNotFail_IfUserIsAnonmyous() + { + $systemSettings = $this->makePluginSettings(); + $systemSettings->customPiwikSiteId->setValue(1); + $systemSettings->customPiwikSiteUrl->setValue('https://demo-anonymous.matomo.org/piwik.php'); + + FakeAccess::clearAccess($superUser = false, array(), array(), $login = 'anonymous'); + + $out = ''; + Piwik::postEvent('Template.jsGlobalVariables', array(&$out)); + self::assertStringContainsString('var piwikUsageTracking = {"targets":[{"url"', $out); + self::assertStringContainsString('{"id":1,"name":"Access","value":"anonymous"}', $out); + } + + public function test_shouldAddTrackingCallsWithoutTargetsAndCustomVariables_IfOptOutIsDisabled() + { + $settings = $this->makePluginSettings(); + $settings->canUserOptOut->setValue(false); + $settings->customPiwikSiteId->setValue(1); + $settings->customPiwikSiteUrl->setValue('https://demo-anonymous.matomo.org/piwik.php'); + $settings->save(); + + // we need to save it first and create new settings instance since the setting will be missing afterwards. + $this->makePluginSettings(); + + $out = ''; + Piwik::postEvent('Template.jsGlobalVariables', array(&$out)); + self::assertStringContainsString('var piwikUsageTracking = {"targets":[{"url":"https:\/\/demo-anonymous.matomo.org\/piwik.php","idSite":1,"useAnonymization":true,"token_auth":null}],"visitorCustomVariables":[{"id":1,"name":"Access","value":"superuser"}],"trackingDomain":"https:\/\/demo-anonymous.matomo.org","exampleDomain":"http:\/\/example.com","userId":"superUserLogin"}', $out); + } + + private function makePluginSettings() + { + $settings = new SystemSettings(); + StaticContainer::getContainer()->set('Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings', $settings); + return $settings; + } + + private function makeUserSettings() + { + $settings = new UserSettings($this->makePluginSettings()); + StaticContainer::getContainer()->set('Piwik\Plugins\AnonymousPiwikUsageMeasurement\UserSettings', $settings); + return $settings; + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess() + ); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/SystemSettingsTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/SystemSettingsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3e7b9fcd35cc68f13b31cf51461ca34420aebaad --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/SystemSettingsTest.php @@ -0,0 +1,138 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Integration; + +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group AnonymousPiwikUsageMeasurement + * @group Settings + * @group SystemSettingsTest + * @group Plugins + */ +class SystemSettingsTest extends IntegrationTestCase +{ + private $idSite = 1; + + /** + * @var SystemSettings + */ + private $settings; + + public function setUp(): void + { + parent::setUp(); + + if (!Fixture::siteCreated($this->idSite)) { + Fixture::createWebsite('2014-01-01 00:00:00'); + } + + $this->settings = new SystemSettings(); + } + + public function test_canUserOptOut_ShouldBeEnabledByDefault() + { + $this->assertSame(true, $this->settings->canUserOptOut->getValue()); + } + + public function test_ownPiwikSiteId_shouldBeZeroByDefault() + { + $this->assertSame(0, $this->settings->ownPiwikSiteId->getValue()); + } + + public function test_ownPiwikSiteId_shouldNotThrowAnException_WhenSavingEmptyValue() + { + $this->settings->ownPiwikSiteId->setValue(''); + + $this->assertSame(0, $this->settings->ownPiwikSiteId->getValue()); + } + + public function test_ownPiwikSiteId_shouldThrowAnException_IfValueIsNotNumeric() + { + $this->expectException(\Exception::class); + $this->expectDeprecationMessage("Site Id 'MyTest0' should be a number"); + $this->settings->ownPiwikSiteId->setValue('MyTest0'); + } + + public function test_ownPiwikSiteId_shouldThrowAnException_IfSiteIdDoesNotExist() + { + $this->expectException(\Exception::class); + $this->expectDeprecationMessage("The specified idSite '5' does not exist"); + $this->settings->ownPiwikSiteId->setValue('5'); + } + + public function test_ownPiwikSiteId_shouldSaveValue_IfSiteIdExistsAndIsNumeric() + { + $this->settings->ownPiwikSiteId->setValue($this->idSite); + $this->assertSame($this->idSite, $this->settings->ownPiwikSiteId->getValue()); + } + + public function test_customPiwikSiteUrl_shouldBeAnEmptyStringByDefault() + { + $this->assertSame('', $this->settings->customPiwikSiteUrl->getValue()); + } + + public function test_customPiwikSiteUrl_shouldAppendPiwikPhpIfNeeded() + { + $this->settings->customPiwikSiteUrl->setValue('http://example.com'); + $this->assertSame('http://example.com/matomo.php', $this->settings->customPiwikSiteUrl->getValue()); + + $this->settings->customPiwikSiteUrl->setValue('http://example.com/'); + $this->assertSame('http://example.com/matomo.php', $this->settings->customPiwikSiteUrl->getValue()); + + $this->settings->customPiwikSiteUrl->setValue('http://example.com/piwik.php'); + $this->assertSame('http://example.com/piwik.php', $this->settings->customPiwikSiteUrl->getValue()); + + $this->settings->customPiwikSiteUrl->setValue('http://example.com/matomo.php'); + $this->assertSame('http://example.com/matomo.php', $this->settings->customPiwikSiteUrl->getValue()); + } + + public function test_customPiwikSiteUrl_shouldThrowAnException_WhenNotAValidUrlIsSet() + { + $this->expectException(\Exception::class); + $this->expectDeprecationMessage("URL 'http:/Valtes.com/idUrl' seems to be not a valid URL"); + $this->settings->customPiwikSiteUrl->setValue('http:/Valtes.com/idUrl'); + } + + public function test_ownPiwikSiteUrl_shouldNotThrowAnException_WhenSavingEmptyValue() + { + $this->settings->customPiwikSiteUrl->setValue(''); + + $this->assertSame('', $this->settings->customPiwikSiteUrl->getValue()); + } + + public function test_customPiwikSiteId_shouldBeZeroByDefault() + { + $this->assertSame(0, $this->settings->customPiwikSiteId->getValue()); + } + + public function test_customPiwikSiteId_shouldNotThrowAnException_WhenSavingEmptyValue() + { + $this->settings->customPiwikSiteId->setValue(''); + + $this->assertSame(0, $this->settings->customPiwikSiteId->getValue()); + } + + public function test_customPiwikSiteId_shouldSaveAnySiteId_WithoutCheckForExistenceOnRemoteSystem() + { + $this->settings->customPiwikSiteId->setValue('95'); + + $this->assertSame(95, $this->settings->customPiwikSiteId->getValue()); + } + + public function test_customPiwikSiteId_shouldThrowAnException_IfValueIsNotNumeric() + { + $this->expectException(\Exception::class); + $this->expectDeprecationMessage("Site Id 'MyTest0' should be a number"); + $this->settings->customPiwikSiteId->setValue('MyTest0'); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/CustomVariablesTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/CustomVariablesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..998b8deab768c5929ec512c2df7f09697b555f52 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/CustomVariablesTest.php @@ -0,0 +1,139 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Integration\Tracker; + +use Piwik\Db; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\CustomVariables; +use Piwik\Plugins\SegmentEditor\API as SegmentApi; +use Piwik\Plugins\UsersManager\API as UsersApi; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\Mock\FakeAccess; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group AnonymousPiwikUsageMeasurement + * @group CustomVariables + * @group CustomVariablesTest + * @group Plugins + */ +class CustomVariablesTest extends IntegrationTestCase +{ + private $idSite = 1; + + /** + * @var CustomVariables + */ + private $customVars; + + public function setUp(): void + { + parent::setUp(); + + if (!Fixture::siteCreated($this->idSite)) { + Fixture::createWebsite('2014-01-01 00:00:00'); + } + + $this->customVars = new CustomVariables(); + } + + public function test_getClientVisitCustomVariables_shouldReturnAccessInformation_IfUser() + { + FakeAccess::clearAccess($superUser = false, $idSitesAdmin = array($this->idSite)); + $access = $this->makeAccessCustomVar('user'); + + $this->assertSame(array($access), $this->customVars->getClientVisitCustomVariables()); + } + + public function test_getClientVisitCustomVariables_shouldReturnAccessInformation_IfAnonymous() + { + FakeAccess::clearAccess($superUser = false, array(), array(), $login = 'anonymous'); + $access = $this->makeAccessCustomVar('anonymous'); + + $this->assertSame(array($access), $this->customVars->getClientVisitCustomVariables()); + } + + public function test_getClientVisitCustomVariables_shouldReturnAccessInformation_IfSuperuser() + { + FakeAccess::clearAccess($superUser = true); + $access = $this->makeAccessCustomVar('superuser'); + + $this->assertSame(array($access), $this->customVars->getClientVisitCustomVariables()); + } + + public function test_getServerVisitCustomVariables_shouldReturnSystemReport() + { + FakeAccess::clearAccess($superUser = true); + + for ($i = 1; $i < 15; $i++) { + if (!Fixture::siteCreated($i)) { + Fixture::createWebsite('2014-01-01 00:00:00'); + } + } + + for ($i = 1; $i < 9; $i++) { + UsersApi::getInstance()->addUser($login = 'test' . $i, 'password0815', "lorem$i@piwik.org"); + } + + for ($i = 1; $i < 5; $i++) { + SegmentApi::getInstance()->add('Segment' . $i, 'pageUrl%3D@inde'); + } + + $customVars = array( + array( + 'id' => 1, + 'name' => 'Matomo Version', + 'value' => '2.14.3', + ), + array( + 'id' => 2, + 'name' => 'PHP Version', + 'value' => '5.5.27', + ), + array( + 'id' => 3, + 'name' => 'Num Users', + 'value' => 8, + ), + array( + 'id' => 4, + 'name' => 'Num Websites', + 'value' => 14, + ), + array( + 'id' => 5, + 'name' => 'Num Segments', + 'value' => 4, + ), + array( + 'id' => 6, + 'name' => 'MySQL Version', + 'value' => Db::get()->getServerVersion(), + ), + ); + + $this->assertSame($customVars, $this->customVars->getServerVisitCustomVariables()); + } + + private function makeAccessCustomVar($permission) + { + return array( + 'id' => 1, + 'name' => 'Access', + 'value' => $permission, + ); + } + + public function provideContainerConfig() + { + return array( + 'Piwik\Access' => new FakeAccess() + ); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/ProfilesTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/ProfilesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..e95e36404cb5060e9953e06099978597b70f2eb2 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/ProfilesTest.php @@ -0,0 +1,139 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Integration\Tracker; + +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Profiles; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group AnonymousPiwikUsageMeasurement + * @group Profiles + * @group ProfilesTest + * @group Plugins + */ +class ProfilesTest extends IntegrationTestCase +{ + /** + * @var Profiles + */ + private $profiles; + + public function setUp(): void + { + parent::setUp(); + + $this->profiles = new Profiles(); + } + + public function test_popAll_ShouldReturnNoProfiles_WhenNothingPushed() + { + $this->assertSame(array(), $this->profiles->popAll()); + } + + public function test_pushEvent_ShouldAddProfileToListOfExistingProfiles() + { + $this->pushProfile('2014-01-01 00:01:02'); + $this->assertEquals(array(array( + 'creation_date' => '2014-01-01 00:01:02', + 'category' => 'Category', + 'name' => 'myName', + 'action' => 'myAction', + 'count' => '1', + 'wall_time' => '35', + )), $this->profiles->popAll()); + } + + public function test_pushEvent_ShouldAggregateValue_WhenPushingSameEventMultipleTimes() + { + $this->pushProfile('2014-01-01 00:01:02'); + $this->pushProfile('2014-01-01 00:01:03'); + $this->pushProfile('2014-01-01 00:01:04'); + $this->assertEquals(array(array( + 'creation_date' => '2014-01-01 00:01:02', + 'category' => 'Category', + 'name' => 'myName', + 'action' => 'myAction', + 'count' => '3', + 'wall_time' => '105', + )), $this->profiles->popAll()); + } + + public function test_pushEvent_ShouldPushAndReturnDifferentProfiles() + { + $this->pushProfile('2014-01-01 00:01:02'); + $this->pushProfile('2014-01-01 00:01:03', 'OtherCategory'); + $this->pushProfile('2014-01-01 00:01:04', 'Category', 'otherName'); + $this->pushProfile('2014-01-01 00:01:05', 'Category', 'myName', 'otherAction'); + $this->pushProfile('2014-01-01 00:01:06', 'Category', 'myName', 'lastAction', 10, 202); + $this->pushProfile('2014-01-01 00:01:07', 'Category', 'myName', 'lastAction', 1, $wallTime = 71); + $this->pushProfile('2014-01-01 00:01:08'); + + $expected = array( + array( + 'creation_date' => '2014-01-01 00:01:06', + 'category' => 'Category', + 'name' => 'myName', + 'action' => 'lastAction', + 'count' => '11', + 'wall_time' => '273', + ), + array( + 'creation_date' => '2014-01-01 00:01:02', + 'category' => 'Category', + 'name' => 'myName', + 'action' => 'myAction', + 'count' => '2', // we tracked same profile twice + 'wall_time' => '70', + ), + array( + 'creation_date' => '2014-01-01 00:01:05', + 'category' => 'Category', + 'name' => 'myName', + 'action' => 'otherAction', + 'count' => '1', + 'wall_time' => '35', + ), + array( + 'creation_date' => '2014-01-01 00:01:04', + 'category' => 'Category', + 'name' => 'otherName', + 'action' => 'myAction', + 'count' => '1', + 'wall_time' => '35', + ), + array( + 'creation_date' => '2014-01-01 00:01:03', + 'category' => 'OtherCategory', + 'name' => 'myName', + 'action' => 'myAction', + 'count' => '1', + 'wall_time' => '35', + ) + ); + + $this->assertEquals($expected, $this->profiles->popAll()); + } + + public function test_popAll_shouldRemoveAllProfiles() + { + $this->pushProfile('2014-01-01 00:01:02'); + $this->pushProfile('2014-01-01 00:01:03', 'OtherCategory'); + $this->pushProfile('2014-01-01 00:01:04', 'Category', 'otherName'); + + $this->assertCount(3, $this->profiles->popAll()); + + $this->assertSame(array(), $this->profiles->popAll()); + } + + private function pushProfile($creationDate, $category = 'Category', $name = 'myName', $action = 'myAction', $count = 1, $wallTime = '35') + { + $this->profiles->pushProfile($creationDate, $category, $name, $action, $count, $wallTime); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/TargetsTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/TargetsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..10a68424beb78f86172fdb658083aadc5bb4b228 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/TargetsTest.php @@ -0,0 +1,132 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Integration\Tracker; + +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Targets; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group AnonymousPiwikUsageMeasurement + * @group Targets + * @group TargetsTest + * @group Plugins + */ +class TargetsTest extends IntegrationTestCase +{ + private $idSite = 1; + + public function setUp(): void + { + parent::setUp(); + + if (!Fixture::siteCreated($this->idSite)) { + Fixture::createWebsite('2014-01-01 00:00:00'); + } + } + + public function test_defaultSettings_ShouldOnlyReturnNone() + { + $settings = $this->makeDefaultSettings(); + + $this->assertTargets(array(), $settings); + } + + public function test_NoTargetEnabled_ShouldReturnNothing() + { + $settings = $this->makeSettingsWithNoInstanceEnabled(); + + $this->assertTargets(array(), $settings); + } + + public function test_TrackToOwnPiwikEnabled_ShouldReturnOwnInstanceTarget() + { + $settings = $this->makeSettingsWithNoInstanceEnabled(); + $settings->ownPiwikSiteId->setValue($this->idSite); + + $ownInstance = $this->getOwnPiwikTarget(); + + $this->assertTargets(array($ownInstance), $settings); + } + + public function test_TrackToCustomPiwikEnabled_ShouldReturnCustomInstanceTarget() + { + $settings = $this->makeSettingsWithNoInstanceEnabled(); + $settings->customPiwikSiteId->setValue(72); + $settings->customPiwikSiteUrl->setValue('http://example.com/piwik'); + + $customInstance = $this->getCustomPiwikTarget('http://example.com/piwik/matomo.php', 72); + + $this->assertTargets(array($customInstance), $settings); + } + + public function test_TrackToCustomPiwikEnabled_ShoulNotReturnAnythingIfNoSiteISSet() + { + $settings = $this->makeSettingsWithNoInstanceEnabled(); + $settings->customPiwikSiteUrl->setValue('http://example.com/piwik'); + $this->assertTargets(array(), $settings); + } + + public function test_TrackToCustomPiwikEnabled_ShoulNotReturnAnythingIfNoUrlIsSet() + { + $settings = $this->makeSettingsWithNoInstanceEnabled(); + $settings->customPiwikSiteId->setValue(72); + $this->assertTargets(array(), $settings); + } + + public function test_AllEnabled_shouldReturnAll() + { + $settings = $this->makeSettingsWithNoInstanceEnabled(); + $settings->ownPiwikSiteId->setValue($this->idSite); + $settings->customPiwikSiteUrl->setValue('http://example.com/matomo'); + $settings->customPiwikSiteId->setValue(73); + + $targets = array( + $this->getOwnPiwikTarget(), + $this->getCustomPiwikTarget('http://example.com/matomo/matomo.php', 73) + ); + + $this->assertTargets($targets, $settings); + } + + private function makeDefaultSettings() + { + return new SystemSettings(); + } + + private function makeSettingsWithNoInstanceEnabled() + { + $settings = $this->makeDefaultSettings(); + return $settings; + } + + private function getOwnPiwikTarget() + { + return $this->getCustomPiwikTarget(Fixture::getRootUrl() . 'tests/PHPUnit/proxy/matomo.php', $this->idSite); + } + + private function getCustomPiwikTarget($url, $idSite) + { + return array( + 'url' => $url, + 'idSite' => $idSite, + 'useAnonymization' => true, + 'token_auth' => null, + ); + } + + private function assertTargets($expectedTargets, $settings) + { + $targets = new Targets($settings); + + $this->assertSame($expectedTargets, $targets->getTargets()); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/TrackersTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/TrackersTest.php new file mode 100644 index 0000000000000000000000000000000000000000..1c531ef5d79cf2d9329f2dd6b3f0e24e1789115e --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/Tracker/TrackersTest.php @@ -0,0 +1,88 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Integration\Tracker; + +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker\Trackers; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\Tracker; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +class TestTargets extends Tracker\Targets +{ + private $targets = array(); + + public function __construct($targets) + { + $this->targets = $targets; + } + + public function getTargets() + { + return $this->targets; + } +} + +/** + * @group AnonymousPiwikUsageMeasurement + * @group Trackers + * @group TrackersTest + * @group Plugins + */ +class TrackersTest extends IntegrationTestCase +{ + /** + * All your actual test methods should start with the name "test" + */ + public function test_makeTrackers_NoTrackersGiven() + { + $targets = new TestTargets(array()); + $trackers = new Trackers($targets); + $trackers = $trackers->makeTrackers(); + + $this->assertSame(array(), $trackers); + } + + /** + * All your actual test methods should start with the name "test" + */ + public function test_makeTrackers_FromDefaultSettings() + { + $settings = new SystemSettings(); + $targets = new Tracker\Targets($settings); + $trackers = new Trackers($targets); + $trackers = $trackers->makeTrackers(); + + $this->assertCount(0, $trackers); + } + + /** + * All your actual test methods should start with the name "test" + */ + public function test_makeTrackers_CanCreateMultipleTrackers_AndSavesUrlAndIdSiteCorrectInTheInstance_AndAnonymizesUrlAndReferrer() + { + $targets = array( + array('url' => 'http://apache.piwik/piwik.php', 'idSite' => 98, 'useAnonymization' => true), + array('url' => 'http://apache.piwik/piwik/piwik.php', 'idSite' => 101, 'useAnonymization' => true), + ); + + $targets = new TestTargets($targets); + $trackers = new Trackers($targets); + $trackers = $trackers->makeTrackers(); + + $this->assertCount(2, $trackers); + + // verify url anonymized, referrer removed, idSite + url applied + $this->assertStringStartsWith('http://apache.piwik/piwik.php?idsite=98&rec=1', $trackers[0]->getUrlTrackPageView()); + $this->assertStringEndsWith('url=https%3A%2F%2Fdemo-anonymous.matomo.org&urlref=', $trackers[0]->getUrlTrackPageView()); + + $this->assertStringStartsWith('http://apache.piwik/piwik/piwik.php?idsite=101&rec=1', $trackers[1]->getUrlTrackPageView()); + $this->assertStringEndsWith('url=https%3A%2F%2Fdemo-anonymous.matomo.org&urlref=', $trackers[1]->getUrlTrackPageView()); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/UserSettingsTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/UserSettingsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d3cc3c159674889d2568eb4adf9430afeb46a6f3 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/Integration/UserSettingsTest.php @@ -0,0 +1,64 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Integration; + +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\SystemSettings; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\UserSettings; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group AnonymousPiwikUsageMeasurement + * @group Settings + * @group UserSettings + * @group UserSettingsTest + * @group Plugins + */ +class UserSettingsTest extends IntegrationTestCase +{ + private $idSite = 1; + + /** + * @var UserSettings + */ + private $settings; + + public function setUp(): void + { + parent::setUp(); + + if (!Fixture::siteCreated($this->idSite)) { + Fixture::createWebsite('2014-01-01 00:00:00'); + } + + $this->settings = new UserSettings(new SystemSettings()); + } + + public function test_userTrackingEnabled_ShouldBeEnabledByDefault() + { + $this->assertSame(true, $this->settings->userTrackingEnabled->getValue()); + } + + public function test_userTrackingEnabled_shouldBeWritableDependingOnOptOut() + { + $settings = $this->createUserSettingsWithUserOptOut(false); + $this->assertSame(false, $settings->userTrackingEnabled->isWritableByCurrentUser()); + + $settings = $this->createUserSettingsWithUserOptOut(true); + $this->assertSame(true, $settings->userTrackingEnabled->isWritableByCurrentUser()); + } + + private function createUserSettingsWithUserOptOut($optOut) + { + $system = new SystemSettings(); + $system->canUserOptOut->setValue($optOut); + return new UserSettings($system); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/CheckDirectDependencyUseCommandTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/System/CheckDirectDependencyUseCommandTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cb6f6c9571a4f9c4f60541c65343b8241c27b280 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/CheckDirectDependencyUseCommandTest.php @@ -0,0 +1,48 @@ +<?php + +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\System; + +use Piwik\Plugins\TestRunner\Commands\CheckDirectDependencyUse; +use Piwik\Tests\Framework\TestCase\SystemTestCase; +use Piwik\Version; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\NullOutput; + +class CheckDirectDependencyUseCommandTest extends SystemTestCase +{ + public function testCommand() + { + if (version_compare(Version::VERSION, '5.0.3', '<=') && !file_exists(PIWIK_INCLUDE_PATH . '/plugins/TestRunner/Commands/CheckDirectDependencyUse.php')) { + $this->markTestSkipped('tests:check-direct-dependency-use is not available in this version'); + } + + $pluginName = 'AnonymousPiwikUsageMeasurement'; + + $checkDirectDependencyUse = new CheckDirectDependencyUse(); + + $console = new \Piwik\Console(self::$fixture->piwikEnvironment); + $console->addCommands([$checkDirectDependencyUse]); + $command = $console->find('tests:check-direct-dependency-use'); + $arguments = [ + 'command' => 'tests:check-direct-dependency-use', + '--plugin' => $pluginName, + '--grep-vendor', + ]; + + $inputObject = new ArrayInput($arguments); + $command->run($inputObject, new NullOutput()); + + $this->assertEquals([ + 'Symfony\Component\Console' => [ + 'AnonymousPiwikUsageMeasurement/tests/System/CheckDirectDependencyUseCommandTest.php' + ], + ], $checkDirectDependencyUse->usesFoundList[$pluginName]); + } +} diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/TasksTest.php b/plugins/AnonymousPiwikUsageMeasurement/tests/System/TasksTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9399172a5e251fa4986fab3f211718c912f04b3b --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/TasksTest.php @@ -0,0 +1,156 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\System; + +use Piwik\DataTable; +use Piwik\Piwik; +use Piwik\Plugins\AnonymousPiwikUsageMeasurement\tests\Fixtures\SendSystemReportTaskFixture; +use Piwik\Tests\Framework\TestCase\SystemTestCase; +use Piwik\Version; + +/** + * @group AnonymousPiwikUsageMeasurement + * @group TasksTest + * @group Plugins + */ +class TasksTest extends SystemTestCase +{ + /** + * @var SendSystemReportTaskFixture + */ + public static $fixture = null; // initialized below class definition + + public function setUp(): void + { + parent::setUp(); + + $self = $this; + Piwik::addAction('API.Request.dispatch.end', function (&$return, $extra) use ($self) { + if ($extra['module'] !== 'Events') { + return; + } + + /** @var DataTable $return*/ + + // we make sure processed result is the same at any time + foreach ($return as &$value) { + $value->setColumn('sum_event_value', '2'); + $value->setColumn('max_event_value', '2'); + $value->setColumn('min_event_value', '2'); + $value->setColumn('sum_daily_nb_uniq_visitors', '2'); + $value->setColumn('avg_event_value', '2'); + + if ($value->isSubtableLoaded()) { + $subtable = $value->getSubtable(); + foreach ($subtable->getRows() as $row) { + $row->setColumn('sum_event_value', '2'); + $row->setColumn('max_event_value', '2'); + $row->setColumn('min_event_value', '2'); + $row->setColumn('sum_daily_nb_uniq_visitors', '2'); + $row->setColumn('avg_event_value', '2'); + } + } + } + }); + } + + /** + * @dataProvider getApiForTesting + */ + public function testApi($api, $params) + { + $this->runApiTests($api, $params); + } + + public function getApiForTesting() + { + $apis = [ + 'API.get', + 'Actions.getPageUrls', + 'Actions.getPageTitles', + 'Events', + 'Referrers.getReferrerType', + 'CustomVariables', + ]; + + $xmlFieldsToRemove = []; + + if (version_compare(Version::VERSION, '5.5.0-b1', '<')) { + $xmlFieldsToRemove = ['Referrers_visitorsFromAIAssistants', 'Referrers_distinctAIAssistants', 'Referrers_visitorsFromAIAssistants_percent']; + } + + if (version_compare(Version::VERSION, '5.6.0-alpha', '<')) { + // In Matomo 5.6 AI agent information had been added + $xmlFieldsToRemove = array_merge($xmlFieldsToRemove, [ + 'nb_visits_ai_agent', + 'nb_actions_ai_agent', + 'nb_uniq_visitors_ai_agent', + 'nb_users_ai_agent', + 'max_actions_ai_agent', + 'bounce_rate_ai_agent', + 'nb_actions_per_visit_ai_agent', + 'avg_time_on_site_ai_agent', + 'nb_visits_human', + 'nb_actions_human', + 'nb_uniq_visitors_human', + 'nb_users_human', + 'max_actions_human', + 'bounce_rate_human', + 'nb_actions_per_visit_human', + 'avg_time_on_site_human', + ]); + } + + if (version_compare(Version::VERSION, '5.7.0-b1', '<')) { + $xmlFieldsToRemove = array_merge($xmlFieldsToRemove, [ + 'BotTracking_AIChatbotsRequests', + 'BotTracking_AIChatbotsAcquiredVisits', + 'BotTracking_AIChatbotsNotFoundRequests', + 'BotTracking_AIChatbotsUniqueChatbots', + 'BotTracking_AIChatbotsServerErrorRequests', + 'BotTracking_AIChatbotsClickThroughRate', + ]); + } + + $apiToTest = []; + foreach ($apis as $api) { + $apiToTest[] = [ + $api, + [ + 'idSite' => 1, + 'date' => 'today', + 'periods' => ['year'], + 'otherRequestParameters' => [ + 'hideColumns' => 'hits,sum_visit_length,entry_sum_visit_length,avg_time_on_site_new,sum_bandwidth,nb_hits_with_bandwidth,min_bandwidth,max_bandwidth,avg_bandwidth,nb_total_overall_bandwidth,nb_total_pageview_bandwidth,nb_total_download_bandwidth', + ], + + // when calling CustomVariables.getUsagesOfSlots, new archives are created until 'today', + // which increments idsubdatatable, but we need to have deterministic idsubdatatable + 'apiNotToCall' => ['CustomVariables.getUsagesOfSlots'], + 'xmlFieldsToRemove' => $xmlFieldsToRemove, + ] + ]; + } + + return $apiToTest; + } + + public static function getOutputPrefix() + { + return ''; + } + + public static function getPathToTestDirectory() + { + return dirname(__FILE__); + } +} + +TasksTest::$fixture = new SendSystemReportTaskFixture(); diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___API.get_year.xml b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___API.get_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..c6e5d18f5fae3d639b73f9ff3788a77afc8bf542 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___API.get_year.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_new>1</nb_visits_new> + <nb_actions_new>39</nb_actions_new> + <max_actions_new>39</max_actions_new> + <bounce_rate_new>0%</bounce_rate_new> + <nb_actions_per_visit_new>39</nb_actions_per_visit_new> + <nb_visits_returning>0</nb_visits_returning> + <nb_actions_returning>0</nb_actions_returning> + <max_actions_returning>0</max_actions_returning> + <bounce_rate_returning>0%</bounce_rate_returning> + <nb_actions_per_visit_returning>0</nb_actions_per_visit_returning> + <avg_time_on_site_returning>0</avg_time_on_site_returning> + <Referrers_visitorsFromSearchEngines>0</Referrers_visitorsFromSearchEngines> + <Referrers_visitorsFromSocialNetworks>0</Referrers_visitorsFromSocialNetworks> + <Referrers_visitorsFromAIAssistants>0</Referrers_visitorsFromAIAssistants> + <Referrers_visitorsFromDirectEntry>1</Referrers_visitorsFromDirectEntry> + <Referrers_visitorsFromWebsites>0</Referrers_visitorsFromWebsites> + <Referrers_visitorsFromCampaigns>0</Referrers_visitorsFromCampaigns> + <Referrers_distinctSearchEngines>0</Referrers_distinctSearchEngines> + <Referrers_distinctSocialNetworks>0</Referrers_distinctSocialNetworks> + <Referrers_distinctAIAssistants>0</Referrers_distinctAIAssistants> + <Referrers_distinctKeywords>0</Referrers_distinctKeywords> + <Referrers_distinctWebsites>0</Referrers_distinctWebsites> + <Referrers_distinctWebsitesUrls>0</Referrers_distinctWebsitesUrls> + <Referrers_distinctCampaigns>0</Referrers_distinctCampaigns> + <PagePerformance_network_time>0</PagePerformance_network_time> + <PagePerformance_network_hits>0</PagePerformance_network_hits> + <PagePerformance_servery_time>0</PagePerformance_servery_time> + <PagePerformance_server_hits>0</PagePerformance_server_hits> + <PagePerformance_transfer_time>0</PagePerformance_transfer_time> + <PagePerformance_transfer_hits>0</PagePerformance_transfer_hits> + <PagePerformance_domprocessing_time>0</PagePerformance_domprocessing_time> + <PagePerformance_domprocessing_hits>0</PagePerformance_domprocessing_hits> + <PagePerformance_domcompletion_time>0</PagePerformance_domcompletion_time> + <PagePerformance_domcompletion_hits>0</PagePerformance_domcompletion_hits> + <PagePerformance_onload_time>0</PagePerformance_onload_time> + <PagePerformance_onload_hits>0</PagePerformance_onload_hits> + <PagePerformance_pageload_time>0</PagePerformance_pageload_time> + <PagePerformance_pageload_hits>0</PagePerformance_pageload_hits> + <avg_time_network>0</avg_time_network> + <avg_time_server>0</avg_time_server> + <avg_time_transfer>0</avg_time_transfer> + <avg_time_dom_processing>0</avg_time_dom_processing> + <avg_time_dom_completion>0</avg_time_dom_completion> + <avg_time_on_load>0</avg_time_on_load> + <avg_page_load_time>0</avg_page_load_time> + <nb_conversions>0</nb_conversions> + <nb_visits_converted>0</nb_visits_converted> + <revenue>0</revenue> + <conversion_rate>0%</conversion_rate> + <nb_conversions_new_visit>0</nb_conversions_new_visit> + <nb_visits_converted_new_visit>0</nb_visits_converted_new_visit> + <revenue_new_visit>0</revenue_new_visit> + <conversion_rate_new_visit>0%</conversion_rate_new_visit> + <nb_conversions_returning_visit>0</nb_conversions_returning_visit> + <nb_visits_converted_returning_visit>0</nb_visits_converted_returning_visit> + <revenue_returning_visit>0</revenue_returning_visit> + <conversion_rate_returning_visit>0%</conversion_rate_returning_visit> + <BotTracking_AIChatbotsRequests>0</BotTracking_AIChatbotsRequests> + <BotTracking_AIChatbotsAcquiredVisits>0</BotTracking_AIChatbotsAcquiredVisits> + <BotTracking_AIChatbotsNotFoundRequests>0</BotTracking_AIChatbotsNotFoundRequests> + <BotTracking_AIChatbotsUniqueChatbots>0</BotTracking_AIChatbotsUniqueChatbots> + <BotTracking_AIChatbotsServerErrorRequests>0</BotTracking_AIChatbotsServerErrorRequests> + <nb_pageviews>1</nb_pageviews> + <nb_uniq_pageviews>1</nb_uniq_pageviews> + <nb_downloads>0</nb_downloads> + <nb_uniq_downloads>0</nb_uniq_downloads> + <nb_outlinks>0</nb_outlinks> + <nb_uniq_outlinks>0</nb_uniq_outlinks> + <nb_searches>0</nb_searches> + <nb_keywords>0</nb_keywords> + <nb_visits_ai_agent>0</nb_visits_ai_agent> + <nb_actions_ai_agent>0</nb_actions_ai_agent> + <max_actions_ai_agent>0</max_actions_ai_agent> + <bounce_rate_ai_agent>0%</bounce_rate_ai_agent> + <nb_actions_per_visit_ai_agent>0</nb_actions_per_visit_ai_agent> + <avg_time_on_site_ai_agent>0</avg_time_on_site_ai_agent> + <nb_visits_human>1</nb_visits_human> + <nb_actions_human>39</nb_actions_human> + <max_actions_human>39</max_actions_human> + <bounce_rate_human>0%</bounce_rate_human> + <nb_actions_per_visit_human>39</nb_actions_per_visit_human> + <avg_time_on_site_human>1</avg_time_on_site_human> + <Referrers_visitorsFromDirectEntry_percent>100%</Referrers_visitorsFromDirectEntry_percent> + <Referrers_visitorsFromSearchEngines_percent>0%</Referrers_visitorsFromSearchEngines_percent> + <Referrers_visitorsFromAIAssistants_percent>0%</Referrers_visitorsFromAIAssistants_percent> + <Referrers_visitorsFromCampaigns_percent>0%</Referrers_visitorsFromCampaigns_percent> + <Referrers_visitorsFromSocialNetworks_percent>0%</Referrers_visitorsFromSocialNetworks_percent> + <Referrers_visitorsFromWebsites_percent>0%</Referrers_visitorsFromWebsites_percent> + <bounce_rate>0%</bounce_rate> + <nb_actions_per_visit>39</nb_actions_per_visit> + <avg_time_on_site>1</avg_time_on_site> + <BotTracking_AIChatbotsClickThroughRate>0</BotTracking_AIChatbotsClickThroughRate> +</result> diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Actions.getPageTitles_year.xml b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Actions.getPageTitles_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..0ac1141d92a6f6ebe4e4a908d1cec3697e83ce4f --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Actions.getPageTitles_year.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label> System-Report</label> + <nb_visits>1</nb_visits> + <nb_hits>1</nb_hits> + <sum_time_spent>0</sum_time_spent> + <entry_nb_visits>1</entry_nb_visits> + <entry_nb_actions>39</entry_nb_actions> + <entry_bounce_count>0</entry_bounce_count> + <exit_nb_visits>1</exit_nb_visits> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_entry_nb_uniq_visitors>1</sum_daily_entry_nb_uniq_visitors> + <sum_daily_exit_nb_uniq_visitors>1</sum_daily_exit_nb_uniq_visitors> + <avg_page_load_time>0</avg_page_load_time> + <avg_time_on_page>0</avg_time_on_page> + <bounce_rate>0%</bounce_rate> + <exit_rate>100%</exit_rate> + <segment>pageTitle==System-Report</segment> + </row> +</result> \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Actions.getPageUrls_year.xml b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Actions.getPageUrls_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..78df702c5d01fe60399e8afb93c90c0f78c9785e --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Actions.getPageUrls_year.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>/system-report</label> + <nb_visits>1</nb_visits> + <nb_hits>1</nb_hits> + <sum_time_spent>0</sum_time_spent> + <entry_nb_visits>1</entry_nb_visits> + <entry_nb_actions>39</entry_nb_actions> + <entry_bounce_count>0</entry_bounce_count> + <exit_nb_visits>1</exit_nb_visits> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_entry_nb_uniq_visitors>1</sum_daily_entry_nb_uniq_visitors> + <sum_daily_exit_nb_uniq_visitors>1</sum_daily_exit_nb_uniq_visitors> + <avg_page_load_time>0</avg_page_load_time> + <avg_time_on_page>0</avg_time_on_page> + <bounce_rate>0%</bounce_rate> + <exit_rate>100%</exit_rate> + <url>https://demo-anonymous.matomo.org/system-report</url> + <segment>pageUrl==https%253A%252F%252Fdemo-anonymous.matomo.org%252Fsystem-report</segment> + </row> +</result> \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___CustomVariables.getCustomVariables_year.xml b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___CustomVariables.getCustomVariables_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..0237db011fdbd7662ff798b874ecfa07dc333f0f --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___CustomVariables.getCustomVariables_year.xml @@ -0,0 +1,148 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Matomo Version</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + <slots> + <row> + <scope>visit</scope> + <index>1</index> + </row> + </slots> + <segment>customVariableName==Matomo+Version</segment> + <subtable> + <row> + <label>2.14.3</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + </row> + </subtable> + </row> + <row> + <label>Num Segments</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + <slots> + <row> + <scope>visit</scope> + <index>5</index> + </row> + </slots> + <segment>customVariableName==Num+Segments</segment> + <subtable> + <row> + <label>0</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + </row> + </subtable> + </row> + <row> + <label>Num Users</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + <slots> + <row> + <scope>visit</scope> + <index>3</index> + </row> + </slots> + <segment>customVariableName==Num+Users</segment> + <subtable> + <row> + <label>1</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + </row> + </subtable> + </row> + <row> + <label>Num Websites</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + <slots> + <row> + <scope>visit</scope> + <index>4</index> + </row> + </slots> + <segment>customVariableName==Num+Websites</segment> + <subtable> + <row> + <label>1</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + </row> + </subtable> + </row> + <row> + <label>PHP Version</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + <slots> + <row> + <scope>visit</scope> + <index>2</index> + </row> + </slots> + <segment>customVariableName==PHP+Version</segment> + <subtable> + <row> + <label>5.5.27</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + </row> + </subtable> + </row> +</result> \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getAction_year.xml b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getAction_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..05824dcbde2d32f69189f4a2f78733012ed5186e --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getAction_year.xml @@ -0,0 +1,953 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>API.getPiwikVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==API.getPiwikVersion+%28count%29</segment> + <subtable> + <row> + <label>API (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>API.getPiwikVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==API.getPiwikVersion+%28speed%29</segment> + <subtable> + <row> + <label>API (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>API.getSettings (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==API.getSettings+%28count%29</segment> + <subtable> + <row> + <label>API (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>API.getSettings (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==API.getSettings+%28speed%29</segment> + <subtable> + <row> + <label>API (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>CoreAdminHome.invalidateArchivedReports (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==CoreAdminHome.invalidateArchivedReports+%28count%29</segment> + <subtable> + <row> + <label>CoreAdminHome (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>CoreAdminHome.invalidateArchivedReports (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==CoreAdminHome.invalidateArchivedReports+%28speed%29</segment> + <subtable> + <row> + <label>CoreAdminHome (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>CustomDimensions.getConfiguredCustomDimensions (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==CustomDimensions.getConfiguredCustomDimensions+%28count%29</segment> + <subtable> + <row> + <label>CustomDimensions (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>CustomDimensions.getConfiguredCustomDimensions (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==CustomDimensions.getConfiguredCustomDimensions+%28speed%29</segment> + <subtable> + <row> + <label>CustomDimensions (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>Dashboard.getDashboards (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==Dashboard.getDashboards+%28count%29</segment> + <subtable> + <row> + <label>Dashboard (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>Dashboard.getDashboards (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==Dashboard.getDashboards+%28speed%29</segment> + <subtable> + <row> + <label>Dashboard (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>Goals.getGoals (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==Goals.getGoals+%28count%29</segment> + <subtable> + <row> + <label>Goals (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>Goals.getGoals (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==Goals.getGoals+%28speed%29</segment> + <subtable> + <row> + <label>Goals (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>LanguagesManager.uses12HourClockForUser (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==LanguagesManager.uses12HourClockForUser+%28count%29</segment> + <subtable> + <row> + <label>LanguagesManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>LanguagesManager.uses12HourClockForUser (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==LanguagesManager.uses12HourClockForUser+%28speed%29</segment> + <subtable> + <row> + <label>LanguagesManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>SitesManager.getAllSites (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==SitesManager.getAllSites+%28count%29</segment> + <subtable> + <row> + <label>SitesManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>SitesManager.getAllSites (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==SitesManager.getAllSites+%28speed%29</segment> + <subtable> + <row> + <label>SitesManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>SitesManager.getSiteFromId (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==SitesManager.getSiteFromId+%28count%29</segment> + <subtable> + <row> + <label>SitesManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>SitesManager.getSiteFromId (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==SitesManager.getSiteFromId+%28speed%29</segment> + <subtable> + <row> + <label>SitesManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.addContainer (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.addContainer+%28count%29</segment> + <subtable> + <row> + <label>TagManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.addContainer (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.addContainer+%28speed%29</segment> + <subtable> + <row> + <label>TagManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.addContainerTag (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.addContainerTag+%28count%29</segment> + <subtable> + <row> + <label>TagManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.addContainerTag (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.addContainerTag+%28speed%29</segment> + <subtable> + <row> + <label>TagManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.addContainerTrigger (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.addContainerTrigger+%28count%29</segment> + <subtable> + <row> + <label>TagManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.addContainerTrigger (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.addContainerTrigger+%28speed%29</segment> + <subtable> + <row> + <label>TagManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.addContainerVariable (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.addContainerVariable+%28count%29</segment> + <subtable> + <row> + <label>TagManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.addContainerVariable (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.addContainerVariable+%28speed%29</segment> + <subtable> + <row> + <label>TagManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.createContainerVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.createContainerVersion+%28count%29</segment> + <subtable> + <row> + <label>TagManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.createContainerVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.createContainerVersion+%28speed%29</segment> + <subtable> + <row> + <label>TagManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.createDefaultContainerForSite (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.createDefaultContainerForSite+%28count%29</segment> + <subtable> + <row> + <label>TagManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.createDefaultContainerForSite (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.createDefaultContainerForSite+%28speed%29</segment> + <subtable> + <row> + <label>TagManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.getAvailableEnvironments (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.getAvailableEnvironments+%28count%29</segment> + <subtable> + <row> + <label>TagManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.getAvailableEnvironments (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.getAvailableEnvironments+%28speed%29</segment> + <subtable> + <row> + <label>TagManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.publishContainerVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.publishContainerVersion+%28count%29</segment> + <subtable> + <row> + <label>TagManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager.publishContainerVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==TagManager.publishContainerVersion+%28speed%29</segment> + <subtable> + <row> + <label>TagManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>UsersManager.getUsers (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==UsersManager.getUsers+%28count%29</segment> + <subtable> + <row> + <label>UsersManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>UsersManager.getUsers (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==UsersManager.getUsers+%28speed%29</segment> + <subtable> + <row> + <label>UsersManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>VisitsSummary.get (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==VisitsSummary.get+%28count%29</segment> + <subtable> + <row> + <label>VisitsSummary (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>VisitsSummary.get (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventAction==VisitsSummary.get+%28speed%29</segment> + <subtable> + <row> + <label>VisitsSummary (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> +</result> \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getCategory_year.xml b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getCategory_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..b24535d39bb62ce0af669cd947d52a7ef089d556 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getCategory_year.xml @@ -0,0 +1,449 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>API (count)</label> + <nb_visits>19</nb_visits> + <nb_events>19</nb_events> + <nb_events_with_value>19</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventCategory==API+%28count%29</segment> + <subtable> + <row> + <label>API.getPiwikVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>API.getSettings (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>CoreAdminHome.invalidateArchivedReports (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>CustomDimensions.getConfiguredCustomDimensions (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>Dashboard.getDashboards (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>Goals.getGoals (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>LanguagesManager.uses12HourClockForUser (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>SitesManager.getAllSites (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>SitesManager.getSiteFromId (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainer (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerTag (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerTrigger (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerVariable (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.createContainerVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.createDefaultContainerForSite (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.getAvailableEnvironments (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.publishContainerVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>UsersManager.getUsers (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>VisitsSummary.get (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>API (speed)</label> + <nb_visits>19</nb_visits> + <nb_events>19</nb_events> + <nb_events_with_value>19</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventCategory==API+%28speed%29</segment> + <subtable> + <row> + <label>API.getPiwikVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>API.getSettings (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>CoreAdminHome.invalidateArchivedReports (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>CustomDimensions.getConfiguredCustomDimensions (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>Dashboard.getDashboards (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>Goals.getGoals (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>LanguagesManager.uses12HourClockForUser (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>SitesManager.getAllSites (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>SitesManager.getSiteFromId (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainer (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerTag (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerTrigger (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerVariable (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.createContainerVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.createDefaultContainerForSite (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.getAvailableEnvironments (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.publishContainerVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>UsersManager.getUsers (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>VisitsSummary.get (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> +</result> \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getName_year.xml b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getName_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..323ce0c0af2eaa44539a4f1ec80b829c77e1fa51 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Events.getName_year.xml @@ -0,0 +1,701 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>TagManager (count)</label> + <nb_visits>8</nb_visits> + <nb_events>8</nb_events> + <nb_events_with_value>8</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==TagManager+%28count%29</segment> + <subtable> + <row> + <label>TagManager.addContainer (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerTag (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerTrigger (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerVariable (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.createContainerVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.createDefaultContainerForSite (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.getAvailableEnvironments (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.publishContainerVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>TagManager (speed)</label> + <nb_visits>8</nb_visits> + <nb_events>8</nb_events> + <nb_events_with_value>8</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==TagManager+%28speed%29</segment> + <subtable> + <row> + <label>TagManager.addContainer (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerTag (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerTrigger (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.addContainerVariable (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.createContainerVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.createDefaultContainerForSite (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.getAvailableEnvironments (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>TagManager.publishContainerVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>API (count)</label> + <nb_visits>2</nb_visits> + <nb_events>2</nb_events> + <nb_events_with_value>2</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==API+%28count%29</segment> + <subtable> + <row> + <label>API.getPiwikVersion (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>API.getSettings (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>API (speed)</label> + <nb_visits>2</nb_visits> + <nb_events>2</nb_events> + <nb_events_with_value>2</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==API+%28speed%29</segment> + <subtable> + <row> + <label>API.getPiwikVersion (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>API.getSettings (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>SitesManager (count)</label> + <nb_visits>2</nb_visits> + <nb_events>2</nb_events> + <nb_events_with_value>2</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==SitesManager+%28count%29</segment> + <subtable> + <row> + <label>SitesManager.getAllSites (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>SitesManager.getSiteFromId (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>SitesManager (speed)</label> + <nb_visits>2</nb_visits> + <nb_events>2</nb_events> + <nb_events_with_value>2</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==SitesManager+%28speed%29</segment> + <subtable> + <row> + <label>SitesManager.getAllSites (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + <row> + <label>SitesManager.getSiteFromId (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>CoreAdminHome (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==CoreAdminHome+%28count%29</segment> + <subtable> + <row> + <label>CoreAdminHome.invalidateArchivedReports (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>CoreAdminHome (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==CoreAdminHome+%28speed%29</segment> + <subtable> + <row> + <label>CoreAdminHome.invalidateArchivedReports (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>CustomDimensions (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==CustomDimensions+%28count%29</segment> + <subtable> + <row> + <label>CustomDimensions.getConfiguredCustomDimensions (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>CustomDimensions (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==CustomDimensions+%28speed%29</segment> + <subtable> + <row> + <label>CustomDimensions.getConfiguredCustomDimensions (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>Dashboard (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==Dashboard+%28count%29</segment> + <subtable> + <row> + <label>Dashboard.getDashboards (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>Dashboard (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==Dashboard+%28speed%29</segment> + <subtable> + <row> + <label>Dashboard.getDashboards (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>Goals (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==Goals+%28count%29</segment> + <subtable> + <row> + <label>Goals.getGoals (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>Goals (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==Goals+%28speed%29</segment> + <subtable> + <row> + <label>Goals.getGoals (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>LanguagesManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==LanguagesManager+%28count%29</segment> + <subtable> + <row> + <label>LanguagesManager.uses12HourClockForUser (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>LanguagesManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==LanguagesManager+%28speed%29</segment> + <subtable> + <row> + <label>LanguagesManager.uses12HourClockForUser (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>UsersManager (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==UsersManager+%28count%29</segment> + <subtable> + <row> + <label>UsersManager.getUsers (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>UsersManager (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==UsersManager+%28speed%29</segment> + <subtable> + <row> + <label>UsersManager.getUsers (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>VisitsSummary (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==VisitsSummary+%28count%29</segment> + <subtable> + <row> + <label>VisitsSummary.get (count)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> + <row> + <label>VisitsSummary (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + <segment>eventName==VisitsSummary+%28speed%29</segment> + <subtable> + <row> + <label>VisitsSummary.get (speed)</label> + <nb_visits>1</nb_visits> + <nb_events>1</nb_events> + <nb_events_with_value>1</nb_events_with_value> + <sum_event_value>2</sum_event_value> + <min_event_value>2</min_event_value> + <max_event_value>2</max_event_value> + <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors> + <avg_event_value>2</avg_event_value> + </row> + </subtable> + </row> +</result> \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Referrers.getReferrerType_year.xml b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Referrers.getReferrerType_year.xml new file mode 100644 index 0000000000000000000000000000000000000000..b6ab0527aeaad6b667e98e1a628179f826dd5138 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/System/expected/test___Referrers.getReferrerType_year.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>Direct Entry</label> + <nb_visits>1</nb_visits> + <nb_actions>39</nb_actions> + <max_actions>39</max_actions> + <bounce_count>0</bounce_count> + <nb_visits_converted>0</nb_visits_converted> + <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors> + <sum_daily_nb_users>0</sum_daily_nb_users> + <segment>referrerType==direct</segment> + <referrer_type>1</referrer_type> + </row> +</result> \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/UI/.gitignore b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f39be478e7a412cafbcc27f53a9875baaa92b664 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/.gitignore @@ -0,0 +1,2 @@ +/processed-ui-screenshots +/screenshot-diffs \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/UI/AnonymousPiwikUsageMeasurement_spec.js b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/AnonymousPiwikUsageMeasurement_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b03f8cec0ca230a27ee72b13390a0b1ca056e4e5 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/AnonymousPiwikUsageMeasurement_spec.js @@ -0,0 +1,35 @@ +/*! + * Matomo - free/libre analytics platform + * + * Screenshot integration tests. + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe("AnonymousPiwikUsageMeasurement", function () { + this.timeout(0); + + // uncomment this if you want to define a custom fixture to load before the test instead of the default one + // this.fixture = "Piwik\\Plugins\\AnonymousPiwikUsageMeasurement\\tests\\Fixtures\\YOUR_FIXTURE_NAME"; + + before(function () { + testEnvironment.pluginsToLoad = ['AnonymousPiwikUsageMeasurement']; + testEnvironment.save(); + }); + + let selector = '.card-content:contains(\'AnonymousPiwikUsageMeasurement\')'; + + it("should display the admin settings page", async function () { + await page.goto("?module=CoreAdminHome&action=generalSettings&idSite=1&period=day&date=yesterday"); + + expect(await page.screenshotSelector(selector)).to.matchImage('admin_settings_page'); + }); + + it("should display the user settings page", async function () { + await page.goto("?module=UsersManager&action=userSettings&idSite=1&period=day&date=yesterday"); + + expect(await page.screenshotSelector(selector)).to.matchImage('user_settings_page'); + }); + +}); \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/UI/expected-ui-screenshots/.gitkeep b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/expected-ui-screenshots/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/UI/expected-ui-screenshots/AnonymousPiwikUsageMeasurement_admin_settings_page.png b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/expected-ui-screenshots/AnonymousPiwikUsageMeasurement_admin_settings_page.png new file mode 100644 index 0000000000000000000000000000000000000000..25f9fb139cad32861706b05441b64b6b14b50aee Binary files /dev/null and b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/expected-ui-screenshots/AnonymousPiwikUsageMeasurement_admin_settings_page.png differ diff --git a/plugins/AnonymousPiwikUsageMeasurement/tests/UI/expected-ui-screenshots/AnonymousPiwikUsageMeasurement_user_settings_page.png b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/expected-ui-screenshots/AnonymousPiwikUsageMeasurement_user_settings_page.png new file mode 100644 index 0000000000000000000000000000000000000000..e21c212e9c7f9a12b8bebbee402bcf93575025b7 Binary files /dev/null and b/plugins/AnonymousPiwikUsageMeasurement/tests/UI/expected-ui-screenshots/AnonymousPiwikUsageMeasurement_user_settings_page.png differ diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/dist/AnonymousPiwikUsageMeasurement.umd.js b/plugins/AnonymousPiwikUsageMeasurement/vue/dist/AnonymousPiwikUsageMeasurement.umd.js new file mode 100644 index 0000000000000000000000000000000000000000..280da00686b7297e97faf010fc204e2c44bfc912 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/dist/AnonymousPiwikUsageMeasurement.umd.js @@ -0,0 +1,399 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("CoreHome"), require("vue")); + else if(typeof define === 'function' && define.amd) + define(["CoreHome", ], factory); + else if(typeof exports === 'object') + exports["AnonymousPiwikUsageMeasurement"] = factory(require("CoreHome"), require("vue")); + else + root["AnonymousPiwikUsageMeasurement"] = factory(root["CoreHome"], root["Vue"]); +})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__19dc__, __WEBPACK_EXTERNAL_MODULE__8bbf__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "plugins/AnonymousPiwikUsageMeasurement/vue/dist/"; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "fae3"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "19dc": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__19dc__; + +/***/ }), + +/***/ "8bbf": +/***/ (function(module, exports) { + +module.exports = __WEBPACK_EXTERNAL_MODULE__8bbf__; + +/***/ }), + +/***/ "fae3": +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, "TrackDashboard", function() { return /* reexport */ TrackDashboard_TrackDashboard; }); +__webpack_require__.d(__webpack_exports__, "TrackDashboardAction", function() { return /* reexport */ TrackDashboard_TrackDashboardAction; }); +__webpack_require__.d(__webpack_exports__, "TrackEmailReports", function() { return /* reexport */ TrackEmailReports_TrackEmailReports; }); +__webpack_require__.d(__webpack_exports__, "TrackMarketplace", function() { return /* reexport */ TrackMarketplace_TrackMarketplace; }); +__webpack_require__.d(__webpack_exports__, "TrackMultiSites", function() { return /* reexport */ TrackMultiSites_TrackMultiSites; }); +__webpack_require__.d(__webpack_exports__, "TrackSegment", function() { return /* reexport */ TrackSegment_TrackSegment; }); + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js +// This file is imported into lib/wc client bundles. + +if (typeof window !== 'undefined') { + var currentScript = window.document.currentScript + if (false) { var getCurrentScript; } + + var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) + if (src) { + __webpack_require__.p = src[1] // eslint-disable-line + } +} + +// Indicate to webpack that this file can be concatenated +/* harmony default export */ var setPublicPath = (null); + +// EXTERNAL MODULE: external "CoreHome" +var external_CoreHome_ = __webpack_require__("19dc"); + +// CONCATENATED MODULE: ./plugins/AnonymousPiwikUsageMeasurement/vue/src/types.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +// CONCATENATED MODULE: ./plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackDashboard/TrackDashboard.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + + +var _window = window, + $ = _window.$; + +function TrackDashboard_onClick() { + var $widget = $(this); + + if (!$widget.parents('.sortable').length) { + return; + } + + var category = 'Dashboard'; + var name = 'Widget'; + var action = $(this).attr('id'); + + window._paq.push(['trackEvent', category, action, name]); +} + +var TrackDashboard = { + mounted: function mounted() { + $('body').on('click', '.widget #close,#minimise,#maximise,#refresh', TrackDashboard_onClick); + }, + unmounted: function unmounted() { + $('body').off('click', '.widget #close,#minimise,#maximise,#refresh', TrackDashboard_onClick); + } +}; +/* harmony default export */ var TrackDashboard_TrackDashboard = (TrackDashboard); +external_CoreHome_["Matomo"].on('Dashboard.Dashboard.mounted', function () { + TrackDashboard.mounted(); +}); +external_CoreHome_["Matomo"].on('Dashboard.Dashboard.unmounted', function () { + TrackDashboard.unmounted(); +}); +// CONCATENATED MODULE: ./plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackDashboard/TrackDashboardAction.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + + +var TrackDashboardAction_window = window, + TrackDashboardAction_$ = TrackDashboardAction_window.$; +var TrackDashboardAction = { + mounted: function mounted(el) { + TrackDashboardAction_$(el).on('click', '[data-action]', function onClick() { + var category = 'Dashboard'; + var name = 'WidgetSelector'; + var action = TrackDashboardAction_$(this).attr('data-action'); + + window._paq.push(['trackEvent', category, action, name]); + }); + } +}; +/* harmony default export */ var TrackDashboard_TrackDashboardAction = (TrackDashboardAction); +external_CoreHome_["Matomo"].on('Dashboard.DashboardSettings.mounted', function (element) { + TrackDashboardAction.mounted(element); +}); +// CONCATENATED MODULE: ./plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackEmailReports/TrackEmailReports.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + + +var TrackEmailReports_window = window, + TrackEmailReports_$ = TrackEmailReports_window.$; +var TrackEmailReports = { + mounted: function mounted(el) { + TrackEmailReports_$(el).on('click', '[name=linkDownloadReport]', function onClick() { + var id = parseInt(TrackEmailReports_$(this).attr('id'), 10); + var url = TrackEmailReports_$(this).closest('td').children('form').attr('action'); + var format = external_CoreHome_["MatomoUrl"].parse(url.split('?')[1] || '').format || 'xml'; // avoid tracking large ids to make sure nobody can identify a specific Piwik instance based + // on that + + id = id % 20; // eslint-disable-line operator-assignment + + var domain = window.piwikUsageTracking.trackingDomain; + var sourceUrl = "".concat(domain, "/scheduledreports/emailreport").concat(id, ".").concat(format); + var linkType = 'download'; + + window._paq.push(['trackLink', sourceUrl, linkType]); + }); + } +}; +/* harmony default export */ var TrackEmailReports_TrackEmailReports = (TrackEmailReports); +external_CoreHome_["Matomo"].on('ScheduledReports.ManageScheduledReport.mounted', function (_ref) { + var element = _ref.element; + TrackEmailReports.mounted(element); +}); +// EXTERNAL MODULE: external {"commonjs":"vue","commonjs2":"vue","root":"Vue"} +var external_commonjs_vue_commonjs2_vue_root_Vue_ = __webpack_require__("8bbf"); + +// CONCATENATED MODULE: ./plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackMarketplace/TrackMarketplace.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + + + +var TrackMarketplace_window = window, + TrackMarketplace_$ = TrackMarketplace_window.$; + +function makeContentBlock($element, pluginName, contentPiece, target) { + $element.attr('data-track-content', ''); + $element.attr('data-content-name', pluginName); + $element.attr('data-content-piece', contentPiece); + $element.attr('data-content-target', target); +} + +var TrackMarketplace = { + mounted: function mounted() { + Object(external_commonjs_vue_commonjs2_vue_root_Vue_["nextTick"])(function () { + TrackMarketplace_$('.marketplace').find('.plugin').each(function (index, plugin) { + var $plugin = TrackMarketplace_$(plugin); + var pluginName = $plugin.find('[matomo-plugin-name]').attr('matomo-plugin-name') || ''; + var header = $plugin.find('.card-title'); + makeContentBlock(header, pluginName, 'Headline', 'popover'); + var body = $plugin.find('.description'); + makeContentBlock(body, pluginName, 'Body', 'popover'); + var footer = $plugin.find('.footer'); + makeContentBlock(footer, pluginName, 'Install', 'self'); + }); + var checkOnScroll = true; + var timeInterval = 0; // disable for better performance + + window._paq.push(['trackVisibleContentImpressions', checkOnScroll, timeInterval]); + }); + } +}; +/* harmony default export */ var TrackMarketplace_TrackMarketplace = (TrackMarketplace); +external_CoreHome_["Matomo"].on('Marketplace.Marketplace.mounted', function () { + TrackMarketplace.mounted(); +}); +// CONCATENATED MODULE: ./plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackMultiSites/TrackMultiSites.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + + +var TrackMultiSites_window = window, + TrackMultiSites_$ = TrackMultiSites_window.$; +var TrackMultiSites = { + mounted: function mounted(el) { + TrackMultiSites_$(el).on('click', '[target=_blank]', function onClick() { + // append ID to not have only one link in report, this way we know the position of the + // clicked outlink + var id = TrackMultiSites_$(this).parents('tr').first().prevAll().length + 1; + var sourceUrl = "".concat(window.piwikUsageTracking.exampleDomain, "/multisites/outlink").concat(id); + var linkType = 'link'; + + window._paq.push(['trackLink', sourceUrl, linkType]); + }); + } +}; +/* harmony default export */ var TrackMultiSites_TrackMultiSites = (TrackMultiSites); +external_CoreHome_["Matomo"].on('MultiSites.MultiSitesSite.mounted', function (_ref) { + var element = _ref.element; + TrackMultiSites.mounted(element); +}); +// CONCATENATED MODULE: ./plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackSegment/TrackSegment.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + + +var TrackSegment_window = window, + TrackSegment_$ = TrackSegment_window.$; + +function trackEvent(action) { + var category = 'Segments'; + var name = 'SegmentEditor'; + return function () { + window._paq.push(['trackEvent', category, action, name]); + }; +} + +var TrackSegment = { + mounted: function mounted(el) { + var element = TrackSegment_$(el); + element.on('click', '[data-idsegment]', trackEvent('Select a segment')); + element.on('mousedown', '.add_new_segment', trackEvent('Add new segment')); + element.on('mousedown', '.saveAndApply', trackEvent('Save')); + element.on('mousedown', '.delete', trackEvent('Delete')); + } +}; +/* harmony default export */ var TrackSegment_TrackSegment = (TrackSegment); +external_CoreHome_["Matomo"].on('piwikSegmentationInited', function () { + TrackSegment.mounted(TrackSegment_$('.segmentListContainer')[0]); +}); +// CONCATENATED MODULE: ./plugins/AnonymousPiwikUsageMeasurement/vue/src/index.ts +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + + + + + + +// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib-no-default.js + + + + +/***/ }) + +/******/ }); +}); +//# sourceMappingURL=AnonymousPiwikUsageMeasurement.umd.js.map \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/dist/AnonymousPiwikUsageMeasurement.umd.min.js b/plugins/AnonymousPiwikUsageMeasurement/vue/dist/AnonymousPiwikUsageMeasurement.umd.min.js new file mode 100644 index 0000000000000000000000000000000000000000..089c340bfd5721e35358a2b155766f45c7abcefc --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/dist/AnonymousPiwikUsageMeasurement.umd.min.js @@ -0,0 +1,36 @@ +(function(e,n){"object"===typeof exports&&"object"===typeof module?module.exports=n(require("CoreHome"),require("vue")):"function"===typeof define&&define.amd?define(["CoreHome"],n):"object"===typeof exports?exports["AnonymousPiwikUsageMeasurement"]=n(require("CoreHome"),require("vue")):e["AnonymousPiwikUsageMeasurement"]=n(e["CoreHome"],e["Vue"])})("undefined"!==typeof self?self:this,(function(e,n){return function(e){var n={};function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:o})},t.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"===typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(t.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)t.d(o,r,function(n){return e[n]}.bind(null,r));return o},t.n=function(e){var n=e&&e.__esModule?function(){return e["default"]}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="plugins/AnonymousPiwikUsageMeasurement/vue/dist/",t(t.s="fae3")}({"19dc":function(n,t){n.exports=e},"8bbf":function(e,t){e.exports=n},fae3:function(e,n,t){"use strict";if(t.r(n),t.d(n,"TrackDashboard",(function(){return s})),t.d(n,"TrackDashboardAction",(function(){return p})),t.d(n,"TrackEmailReports",(function(){return g})),t.d(n,"TrackMarketplace",(function(){return x})),t.d(n,"TrackMultiSites",(function(){return T})),t.d(n,"TrackSegment",(function(){return $})),"undefined"!==typeof window){var o=window.document.currentScript,r=o&&o.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);r&&(t.p=r[1])}var i=t("19dc"),a=window,u=a.$;function c(){var e=u(this);if(e.parents(".sortable").length){var n="Dashboard",t="Widget",o=u(this).attr("id");window._paq.push(["trackEvent",n,o,t])}}var d={mounted:function(){u("body").on("click",".widget #close,#minimise,#maximise,#refresh",c)},unmounted:function(){u("body").off("click",".widget #close,#minimise,#maximise,#refresh",c)}},s=d;i["Matomo"].on("Dashboard.Dashboard.mounted",(function(){d.mounted()})),i["Matomo"].on("Dashboard.Dashboard.unmounted",(function(){d.unmounted()})); +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +var f=window,m=f.$,l={mounted:function(e){m(e).on("click","[data-action]",(function(){var e="Dashboard",n="WidgetSelector",t=m(this).attr("data-action");window._paq.push(["trackEvent",e,t,n])}))}},p=l;i["Matomo"].on("Dashboard.DashboardSettings.mounted",(function(e){l.mounted(e)})); +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +var w=window,v=w.$,k={mounted:function(e){v(e).on("click","[name=linkDownloadReport]",(function(){var e=parseInt(v(this).attr("id"),10),n=v(this).closest("td").children("form").attr("action"),t=i["MatomoUrl"].parse(n.split("?")[1]||"").format||"xml";e%=20;var o=window.piwikUsageTracking.trackingDomain,r="".concat(o,"/scheduledreports/emailreport").concat(e,".").concat(t),a="download";window._paq.push(["trackLink",r,a])}))}},g=k;i["Matomo"].on("ScheduledReports.ManageScheduledReport.mounted",(function(e){var n=e.element;k.mounted(n)}));var b=t("8bbf"),h=window,y=h.$; +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */function M(e,n,t,o){e.attr("data-track-content",""),e.attr("data-content-name",n),e.attr("data-content-piece",t),e.attr("data-content-target",o)}var S={mounted:function(){Object(b["nextTick"])((function(){y(".marketplace").find(".plugin").each((function(e,n){var t=y(n),o=t.find("[matomo-plugin-name]").attr("matomo-plugin-name")||"",r=t.find(".card-title");M(r,o,"Headline","popover");var i=t.find(".description");M(i,o,"Body","popover");var a=t.find(".footer");M(a,o,"Install","self")}));var e=!0,n=0;window._paq.push(["trackVisibleContentImpressions",e,n])}))}},x=S;i["Matomo"].on("Marketplace.Marketplace.mounted",(function(){S.mounted()})); +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +var _=window,D=_.$,j={mounted:function(e){D(e).on("click","[target=_blank]",(function(){var e=D(this).parents("tr").first().prevAll().length+1,n="".concat(window.piwikUsageTracking.exampleDomain,"/multisites/outlink").concat(e),t="link";window._paq.push(["trackLink",n,t])}))}},T=j;i["Matomo"].on("MultiSites.MultiSitesSite.mounted",(function(e){var n=e.element;j.mounted(n)})); +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ +var q=window,A=q.$;function O(e){var n="Segments",t="SegmentEditor";return function(){window._paq.push(["trackEvent",n,e,t])}}var P={mounted:function(e){var n=A(e);n.on("click","[data-idsegment]",O("Select a segment")),n.on("mousedown",".add_new_segment",O("Add new segment")),n.on("mousedown",".saveAndApply",O("Save")),n.on("mousedown",".delete",O("Delete"))}},$=P;i["Matomo"].on("piwikSegmentationInited",(function(){P.mounted(A(".segmentListContainer")[0])}))}})})); +//# sourceMappingURL=AnonymousPiwikUsageMeasurement.umd.min.js.map \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/dist/umd.metadata.json b/plugins/AnonymousPiwikUsageMeasurement/vue/dist/umd.metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..9ecfcc0456204cf8e8e80416ebc95ef42d4fbd28 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/dist/umd.metadata.json @@ -0,0 +1,5 @@ +{ + "dependsOn": [ + "CoreHome" + ] +} \ No newline at end of file diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackDashboard/TrackDashboard.ts b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackDashboard/TrackDashboard.ts new file mode 100644 index 0000000000000000000000000000000000000000..6238b599a8af910abbf33cd049b01fc8a9a732cd --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackDashboard/TrackDashboard.ts @@ -0,0 +1,46 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + +import { Matomo } from 'CoreHome'; +import '../types'; + +const { $ } = window; + +function onClick(this: HTMLElement) { + const $widget = $(this); + + if (!$widget.parents('.sortable').length) { + return; + } + + const category = 'Dashboard'; + const name = 'Widget'; + const action = $(this).attr('id'); + + window._paq.push(['trackEvent', category, action, name]); +} + +const TrackDashboard = { + mounted(): void { + $('body').on('click', '.widget #close,#minimise,#maximise,#refresh', onClick); + }, + unmounted(): void { + $('body').off('click', '.widget #close,#minimise,#maximise,#refresh', onClick); + }, +}; + +export default TrackDashboard; + +Matomo.on('Dashboard.Dashboard.mounted', () => { + TrackDashboard.mounted(); +}); + +Matomo.on('Dashboard.Dashboard.unmounted', () => { + TrackDashboard.unmounted(); +}); diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackDashboard/TrackDashboardAction.ts b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackDashboard/TrackDashboardAction.ts new file mode 100644 index 0000000000000000000000000000000000000000..4490cd3f5c7173c7e973b5d7222ea885bdbc1d9d --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackDashboard/TrackDashboardAction.ts @@ -0,0 +1,31 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + +import { Matomo } from 'CoreHome'; +import '../types'; + +const { $ } = window; + +const TrackDashboardAction = { + mounted(el: HTMLElement): void { + $(el).on('click', '[data-action]', function onClick() { + const category = 'Dashboard'; + const name = 'WidgetSelector'; + const action = $(this).attr('data-action'); + + window._paq.push(['trackEvent', category, action, name]); + }); + }, +}; + +export default TrackDashboardAction; + +Matomo.on('Dashboard.DashboardSettings.mounted', (element: HTMLElement) => { + TrackDashboardAction.mounted(element); +}); diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackEmailReports/TrackEmailReports.ts b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackEmailReports/TrackEmailReports.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7b7e6e92e62aa3e4fe4070c61fb884e81bfd890 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackEmailReports/TrackEmailReports.ts @@ -0,0 +1,42 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + +import { MatomoUrl, Matomo } from 'CoreHome'; +import '../types'; + +const { $ } = window; + +const TrackEmailReports = { + mounted(el: HTMLElement): void { + $(el).on('click', '[name=linkDownloadReport]', function onClick() { + let id = parseInt($(this).attr('id')!, 10); + const url = $(this).closest('td').children('form').attr('action')!; + const format = MatomoUrl.parse(url.split('?')[1] || '').format as string || 'xml'; + + // avoid tracking large ids to make sure nobody can identify a specific Piwik instance based + // on that + id = id % 20; // eslint-disable-line operator-assignment + + const domain = window.piwikUsageTracking.trackingDomain; + const sourceUrl = `${domain}/scheduledreports/emailreport${id}.${format}`; + const linkType = 'download'; + + window._paq.push(['trackLink', sourceUrl, linkType]); + }); + }, +}; + +export default TrackEmailReports; + +Matomo.on( + 'ScheduledReports.ManageScheduledReport.mounted', + ({ element }: { element: HTMLElement }) => { + TrackEmailReports.mounted(element); + }, +); diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackMarketplace/TrackMarketplace.ts b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackMarketplace/TrackMarketplace.ts new file mode 100644 index 0000000000000000000000000000000000000000..b10a733784e11d614572286645b8f4c76ff5087a --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackMarketplace/TrackMarketplace.ts @@ -0,0 +1,56 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + +import { nextTick } from 'vue'; +import { Matomo } from 'CoreHome'; +import '../types'; + +const { $ } = window; + +function makeContentBlock( + $element: JQuery, + pluginName: string, + contentPiece: string, + target: string, +) { + $element.attr('data-track-content', ''); + $element.attr('data-content-name', pluginName); + $element.attr('data-content-piece', contentPiece); + $element.attr('data-content-target', target); +} + +const TrackMarketplace = { + mounted(): void { + nextTick(() => { + $('.marketplace').find('.plugin').each((index, plugin) => { + const $plugin = $(plugin); + const pluginName = $plugin.find('[matomo-plugin-name]').attr('matomo-plugin-name') || ''; + + const header = $plugin.find('.card-title'); + makeContentBlock(header, pluginName, 'Headline', 'popover'); + + const body = $plugin.find('.description'); + makeContentBlock(body, pluginName, 'Body', 'popover'); + + const footer = $plugin.find('.footer'); + makeContentBlock(footer, pluginName, 'Install', 'self'); + }); + + const checkOnScroll = true; + const timeInterval = 0; // disable for better performance + window._paq.push(['trackVisibleContentImpressions', checkOnScroll, timeInterval]); + }); + }, +}; + +export default TrackMarketplace; + +Matomo.on('Marketplace.Marketplace.mounted', () => { + TrackMarketplace.mounted(); +}); diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackMultiSites/TrackMultiSites.ts b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackMultiSites/TrackMultiSites.ts new file mode 100644 index 0000000000000000000000000000000000000000..23a3f6495b3e844ed9356c848bcecb267c6e5355 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackMultiSites/TrackMultiSites.ts @@ -0,0 +1,33 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + +import { Matomo } from 'CoreHome'; +import '../types'; + +const { $ } = window; + +const TrackMultiSites = { + mounted(el: HTMLElement): void { + $(el).on('click', '[target=_blank]', function onClick() { + // append ID to not have only one link in report, this way we know the position of the + // clicked outlink + const id = $(this).parents('tr').first().prevAll().length + 1; + const sourceUrl = `${window.piwikUsageTracking.exampleDomain}/multisites/outlink${id}`; + const linkType = 'link'; + + window._paq.push(['trackLink', sourceUrl, linkType]); + }); + }, +}; + +export default TrackMultiSites; + +Matomo.on('MultiSites.MultiSitesSite.mounted', ({ element }: { element: HTMLElement }) => { + TrackMultiSites.mounted(element); +}); diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackSegment/TrackSegment.ts b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackSegment/TrackSegment.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a6a74537cfe7a5686a7033294df6ce80d4b6440 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/src/TrackSegment/TrackSegment.ts @@ -0,0 +1,38 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + +import { Matomo } from 'CoreHome'; +import '../types'; + +const { $ } = window; + +function trackEvent(action: string) { + const category = 'Segments'; + const name = 'SegmentEditor'; + + return () => { + window._paq.push(['trackEvent', category, action, name]); + }; +} + +const TrackSegment = { + mounted(el: HTMLElement): void { + const element = $(el); + element.on('click', '[data-idsegment]', trackEvent('Select a segment')); + element.on('mousedown', '.add_new_segment', trackEvent('Add new segment')); + element.on('mousedown', '.saveAndApply', trackEvent('Save')); + element.on('mousedown', '.delete', trackEvent('Delete')); + }, +}; + +export default TrackSegment; + +Matomo.on('piwikSegmentationInited', () => { + TrackSegment.mounted($('.segmentListContainer')[0]); +}); diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/src/index.ts b/plugins/AnonymousPiwikUsageMeasurement/vue/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc8b00140ed1b23cd9a83bdb1e0fc4f4d2ae329a --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/src/index.ts @@ -0,0 +1,13 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +export { default as TrackDashboard } from './TrackDashboard/TrackDashboard'; +export { default as TrackDashboardAction } from './TrackDashboard/TrackDashboardAction'; +export { default as TrackEmailReports } from './TrackEmailReports/TrackEmailReports'; +export { default as TrackMarketplace } from './TrackMarketplace/TrackMarketplace'; +export { default as TrackMultiSites } from './TrackMultiSites/TrackMultiSites'; +export { default as TrackSegment } from './TrackSegment/TrackSegment'; diff --git a/plugins/AnonymousPiwikUsageMeasurement/vue/src/types.ts b/plugins/AnonymousPiwikUsageMeasurement/vue/src/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..53a72461b0566c940df9d0b3ee78169ae9b1fb16 --- /dev/null +++ b/plugins/AnonymousPiwikUsageMeasurement/vue/src/types.ts @@ -0,0 +1,24 @@ +/*! + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +/* eslint-disable no-underscore-dangle */ + +export interface MatomoQueue { + push(args: unknown[]): void; +} + +export interface UsageTrackingGlobal { + trackingDomain: string; + exampleDomain: string; +} + +declare global { + interface Window { + _paq: MatomoQueue; + piwikUsageTracking: UsageTrackingGlobal; + } +} diff --git a/plugins/ArchivingMetrics/ArchivingMetrics.php b/plugins/ArchivingMetrics/ArchivingMetrics.php new file mode 100644 index 0000000000000000000000000000000000000000..cd7df09584c76320ae192ae0b8cc5ee0fe566985 --- /dev/null +++ b/plugins/ArchivingMetrics/ArchivingMetrics.php @@ -0,0 +1,66 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics; + +use Piwik\Period; +use Piwik\Segment; +use Piwik\Site; + +class ArchivingMetrics extends \Piwik\Plugin +{ + public function registerEvents() + { + return [ + 'CoreAdminHome.archiveReports.start' => 'onArchiveReportsStart', + 'CoreAdminHome.archiveReports.complete' => 'onArchiveReportsComplete', + ]; + } + + public function onArchiveReportsStart( + int $idSite, + Period $period, + Segment $segment, + string $plugin, + $report, + bool $isArchivePhpTriggered + ): void { + $timer = Timer::getInstance($isArchivePhpTriggered); + $context = $this->buildContext($idSite, $period, $segment, $plugin, $report); + + $timer->start($context); + } + + /** + * @param int[] $idArchives + */ + public function onArchiveReportsComplete( + int $idSite, + Period $period, + Segment $segment, + string $plugin, + $report, + bool $isArchivePhpTriggered, + array $idArchives, + bool $wasCached + ): void { + $timer = Timer::getInstance($isArchivePhpTriggered); + $context = $this->buildContext($idSite, $period, $segment, $plugin, $report); + + $timer->complete($context, $idArchives, $wasCached); + } + + private function buildContext(int $idSite, Period $period, Segment $segment, string $plugin, $report): Context + { + $site = new Site($idSite); + return new Context($idSite, $period, $segment, $plugin, $report, $site->getTimezone()); + } +} diff --git a/plugins/ArchivingMetrics/Clock/Clock.php b/plugins/ArchivingMetrics/Clock/Clock.php new file mode 100644 index 0000000000000000000000000000000000000000..89a32b645d5fe129a8a0bfb605ecd319e4ce8696 --- /dev/null +++ b/plugins/ArchivingMetrics/Clock/Clock.php @@ -0,0 +1,20 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics\Clock; + +final class Clock implements ClockInterface +{ + public function microtime(): float + { + return microtime(true); + } +} diff --git a/plugins/ArchivingMetrics/Clock/ClockInterface.php b/plugins/ArchivingMetrics/Clock/ClockInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..edef8deaeb754ac295dfc0316a488ef2f08c8cc8 --- /dev/null +++ b/plugins/ArchivingMetrics/Clock/ClockInterface.php @@ -0,0 +1,17 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics\Clock; + +interface ClockInterface +{ + public function microtime(): float; +} diff --git a/plugins/ArchivingMetrics/Context.php b/plugins/ArchivingMetrics/Context.php new file mode 100644 index 0000000000000000000000000000000000000000..13d03f551dd70e2e71fae11d1caa89458fe8e2dc --- /dev/null +++ b/plugins/ArchivingMetrics/Context.php @@ -0,0 +1,79 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics; + +use Piwik\Period; +use Piwik\Segment; + +final class Context +{ + /** + * @var int + */ + public $idSite; + + /** + * @var Period + */ + public $period; + + /** + * @var Segment + */ + public $segment; + + /** + * @var string + */ + public $plugin; + + /** + * @var false|string|string[] + */ + public $report; + + /** + * @var string + */ + public $siteTimezone; + + /** + * @param false|string|string[] $report + */ + public function __construct( + int $idSite, + Period $period, + Segment $segment, + string $plugin, + $report = false, + string $siteTimezone = 'UTC' + ) { + $this->idSite = $idSite; + $this->period = $period; + $this->segment = $segment; + $this->plugin = $plugin; + $this->report = $report; + $this->siteTimezone = $siteTimezone; + } + + public function getKey(): string + { + return implode('|', [ + $this->idSite, + $this->period->getLabel(), + $this->segment->getString(), + $this->period->getDateTimeStart()->toString('Y-m-d'), + $this->period->getDateTimeEnd()->toString('Y-m-d'), + $this->plugin, + ]); + } +} diff --git a/plugins/ArchivingMetrics/Tasks.php b/plugins/ArchivingMetrics/Tasks.php new file mode 100644 index 0000000000000000000000000000000000000000..dc702be830e39bdb63057ccc6d7c527906f0a33d --- /dev/null +++ b/plugins/ArchivingMetrics/Tasks.php @@ -0,0 +1,58 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics; + +use Piwik\Common; +use Piwik\Config; +use Piwik\Date; +use Piwik\Db; + +class Tasks extends \Piwik\Plugin\Tasks +{ + public const DEFAULT_RETENTION_DAYS = 180; + + public function schedule() + { + $this->weekly('purgeOldMetrics'); + $this->monthly('purgeMetricsForDeletedSites'); + } + + /** + * To test execute the following command: + * `./console core:run-scheduled-tasks --force "Piwik\Plugins\ArchivingMetrics\Tasks.purgeOldMetrics"` + */ + public function purgeOldMetrics() + { + $retentionDays = $this->getRetentionDays(); + if ($retentionDays <= 0) { + return; + } + + $cutoff = Date::now()->subDay($retentionDays)->getDatetime(); + $table = Common::prefixTable('archiving_metrics'); + Db::query("DELETE FROM {$table} WHERE ts_started < ?", [$cutoff]); + } + + public function purgeMetricsForDeletedSites() + { + $siteTable = Common::prefixTable('site'); + $table = Common::prefixTable('archiving_metrics'); + Db::query("DELETE a FROM {$table} a LEFT JOIN {$siteTable} s ON a.idsite = s.idsite WHERE s.idsite IS NULL"); + } + + private function getRetentionDays(): int + { + $config = Config::getInstance(); + $retentionDays = $config->ArchivingMetrics['retention_days'] ?? self::DEFAULT_RETENTION_DAYS; + return max(0, (int) $retentionDays); + } +} diff --git a/plugins/ArchivingMetrics/Timer.php b/plugins/ArchivingMetrics/Timer.php new file mode 100644 index 0000000000000000000000000000000000000000..2ef19ccfc9ea6ea57709311dd349c83093af16f1 --- /dev/null +++ b/plugins/ArchivingMetrics/Timer.php @@ -0,0 +1,213 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics; + +use Piwik\ArchiveProcessor\Rules; +use Piwik\Date; +use Piwik\Plugins\ArchivingMetrics\Clock\Clock; +use Piwik\Plugins\ArchivingMetrics\Clock\ClockInterface; +use Piwik\Plugins\ArchivingMetrics\Writer\DbWriter; +use Piwik\Plugins\ArchivingMetrics\Writer\WriterInterface; + +final class Timer +{ + /** + * @var bool + */ + private $isArchivePhpTriggered; + + /** + * @var ClockInterface + */ + private $clock; + + /** + * @var WriterInterface + */ + private $writer; + + /** + * @var array<string, array<string, mixed>> + */ + private $runs = []; + + /** + * @var ?Timer + */ + private static $instance; + + public function __construct(bool $isArchivePhpTriggered, ClockInterface $clock, WriterInterface $writer) + { + $this->isArchivePhpTriggered = $isArchivePhpTriggered; + $this->clock = $clock; + $this->writer = $writer; + } + + public static function getInstance(bool $isArchivePhpTriggered, ?ClockInterface $clock = null, ?WriterInterface $writer = null): self + { + if (self::$instance !== null) { + return self::$instance; + } + + if ($clock === null) { + $clock = new Clock(); + } + + if ($writer === null) { + $writer = new DbWriter(); + } + + self::$instance = new self($isArchivePhpTriggered, $clock, $writer); + return self::$instance; + } + + /** + * @internal For tests only. + */ + public static function resetInstanceForTests(): void + { + self::$instance = null; + } + + public function start(Context $context): void + { + if (false === $this->isApplicableForTiming($context)) { + return; + } + + $this->runs[$context->getKey()] = [ + 'context' => $context, + 'timeStarted' => $this->clock->microtime(), + ]; + } + + public function complete(Context $context, array $idArchives, bool $wasCached): void + { + if (false === $this->isApplicableForTiming($context)) { + return; + } + + if (true === $wasCached) { + return; + } + + if (empty($idArchives) || count($idArchives) !== 1) { + return; + } + + $idArchive = reset($idArchives); + $key = $context->getKey(); + + if (!isset($this->runs[$key]['timeStarted'])) { + return; + } + + $finishedAt = $this->clock->microtime(); + $totalTimeMs = ($finishedAt - $this->runs[$key]['timeStarted']); + + $this->runs[$key]['ts_started'] = date('Y-m-d H:i:s', (int) $this->runs[$key]['timeStarted']); + $this->runs[$key]['totalTime'] = $totalTimeMs; + $this->runs[$key]['timeFinished'] = $finishedAt; + $this->runs[$key]['ts_finished'] = date('Y-m-d H:i:s', (int) $this->runs[$key]['timeFinished']); + + $exclusiveTimeMs = $this->calculateExclusiveTime($key); + $this->runs[$key]['exclusiveTime'] = $exclusiveTimeMs; + + /** @var Context $storedContext */ + $storedContext = $this->runs[$key]['context']; + $archiveName = Rules::getDoneStringFlagFor( + [$storedContext->idSite], + $storedContext->segment, + $storedContext->period->getLabel(), + $storedContext->plugin + ); + $this->writer->write($storedContext, [ + 'idarchive' => $idArchive, + 'archive_name' => $archiveName, + 'ts_started' => $this->runs[$key]['ts_started'], + 'ts_finished' => $this->runs[$key]['ts_finished'], + 'total_time' => (int) round($totalTimeMs * 1000), + 'total_time_exclusive' => (int) round($exclusiveTimeMs * 1000), + 'is_temporary' => $this->isTemporaryArchive($storedContext, (int) $this->runs[$key]['timeStarted']), + ]); + } + + private function isTemporaryArchive(Context $context, int $archiveStartTimestamp): int + { + $archiveStartInSiteTimezone = Date::factory($archiveStartTimestamp, $context->siteTimezone); + $periodEndInSiteTimezone = $context->period->getDateTimeEnd()->setTimezone($context->siteTimezone); + return (int) $archiveStartInSiteTimezone->isEarlier($periodEndInSiteTimezone); + } + + private function isApplicableForTiming(Context $context): bool + { + if (false === $this->isArchivePhpTriggered) { + return false; + } + + $doneFlag = Rules::getDoneStringFlagFor( + [$context->idSite], + $context->segment, + $context->period->getLabel(), + $context->plugin + ); + if (strpos($doneFlag, '.') !== false) { + return false; + } + + return true; + } + + private function calculateExclusiveTime(string $currentKey): float + { + if (empty($this->runs[$currentKey])) { + return 0.0; + } + + $current = $this->runs[$currentKey]; + + $totalTimeMs = $current['totalTime']; + + // If the key is last in the array then it's probably not a nested archive so this calculation doesn't matter + if ($currentKey === array_key_last($this->runs)) { + return $totalTimeMs; + } + + $childTotalMs = 0.0; + + foreach ($this->runs as $otherKey => $run) { + if ($otherKey === $currentKey) { + continue; + } + if (empty($run['exclusiveTime'])) { + continue; + } + + $childFinished = $run['timeFinished'] ?? null; + $childStarted = $run['timeStarted'] ?? null; + if ($childFinished === null || $childStarted === null) { + continue; + } + + if ($childFinished <= $current['timeFinished'] && $childStarted >= $current['timeStarted']) { + $childTotalMs += $run['exclusiveTime']; + } + } + + $exclusive = $totalTimeMs - $childTotalMs; + if ($exclusive < 0) { + return $totalTimeMs; + } + + return $exclusive; + } +} diff --git a/plugins/ArchivingMetrics/Writer/DbWriter.php b/plugins/ArchivingMetrics/Writer/DbWriter.php new file mode 100644 index 0000000000000000000000000000000000000000..146ae7cbf55db523f25b3b4780bc38a59f724668 --- /dev/null +++ b/plugins/ArchivingMetrics/Writer/DbWriter.php @@ -0,0 +1,40 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics\Writer; + +use Piwik\Common; +use Piwik\Db; +use Piwik\Plugins\ArchivingMetrics\Context; + +final class DbWriter implements WriterInterface +{ + public function write(Context $context, array $timing): void + { + Db::query( + 'INSERT INTO ' . Common::prefixTable('archiving_metrics') . ' (idarchive, idsite, archive_name, date1, date2, period, ts_started, ts_finished, total_time, total_time_exclusive, is_temporary) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + [ + $timing['idarchive'], + $context->idSite, + $timing['archive_name'], + $context->period->getDateTimeStart()->toString('Y-m-d'), + $context->period->getDateTimeEnd()->toString('Y-m-d'), + $context->period->getId(), + $timing['ts_started'], + $timing['ts_finished'], + $timing['total_time'], + $timing['total_time_exclusive'], + $timing['is_temporary'], + ] + ); + } +} diff --git a/plugins/ArchivingMetrics/Writer/WriterInterface.php b/plugins/ArchivingMetrics/Writer/WriterInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..00ae4a742400c2e196d5a6c617f02e391bb9ea33 --- /dev/null +++ b/plugins/ArchivingMetrics/Writer/WriterInterface.php @@ -0,0 +1,19 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics\Writer; + +use Piwik\Plugins\ArchivingMetrics\Context; + +interface WriterInterface +{ + public function write(Context $context, array $timing): void; +} diff --git a/plugins/ArchivingMetrics/lang/am.json b/plugins/ArchivingMetrics/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/am.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ar.json b/plugins/ArchivingMetrics/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ar.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/az.json b/plugins/ArchivingMetrics/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/az.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/be.json b/plugins/ArchivingMetrics/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/be.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/bg.json b/plugins/ArchivingMetrics/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/bg.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/bn.json b/plugins/ArchivingMetrics/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/bn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/bs.json b/plugins/ArchivingMetrics/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/bs.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ca.json b/plugins/ArchivingMetrics/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ca.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/cs.json b/plugins/ArchivingMetrics/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/cs.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/cy.json b/plugins/ArchivingMetrics/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/da.json b/plugins/ArchivingMetrics/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/da.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/de.json b/plugins/ArchivingMetrics/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..017e0036f2b814054a6b884bf953ae49c1cc9cc2 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/de.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "Zeichnet Metriken über die Archivierungsleistung auf." + } +} diff --git a/plugins/ArchivingMetrics/lang/dv.json b/plugins/ArchivingMetrics/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/el.json b/plugins/ArchivingMetrics/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..bcfc88fba4cc77a463e8063577b93df805b3a143 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/el.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "Καταγράφει μετρικές σχετικά με την απόδοση της αρχειοθέτησης." + } +} diff --git a/plugins/ArchivingMetrics/lang/en.json b/plugins/ArchivingMetrics/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..8fb697022ea03f8a6fb32a8ccaa2fc001fabe3d2 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/en.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "Records metrics about archiving performance." + } +} diff --git a/plugins/ArchivingMetrics/lang/eo.json b/plugins/ArchivingMetrics/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/eo.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/es-ar.json b/plugins/ArchivingMetrics/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/es-ar.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/es.json b/plugins/ArchivingMetrics/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/es.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/et.json b/plugins/ArchivingMetrics/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/et.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/eu.json b/plugins/ArchivingMetrics/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/eu.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/fa.json b/plugins/ArchivingMetrics/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/fa.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/fi.json b/plugins/ArchivingMetrics/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/fi.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/fr.json b/plugins/ArchivingMetrics/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..75d0accaa61c755cc897e9d0366af6bc864f1ce8 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/fr.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "Enregistre des métriques sur les performances de l'archivage." + } +} diff --git a/plugins/ArchivingMetrics/lang/ga.json b/plugins/ArchivingMetrics/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..ebd0f59196113bca0023a017409aeb0ca99e14f6 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ga.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "Taifeadann sé méadrachtaí faoi fheidhmíocht cartlannúcháin." + } +} diff --git a/plugins/ArchivingMetrics/lang/gl.json b/plugins/ArchivingMetrics/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/gl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/gu.json b/plugins/ArchivingMetrics/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/gu.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/he.json b/plugins/ArchivingMetrics/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/he.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/hi.json b/plugins/ArchivingMetrics/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/hi.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/hr.json b/plugins/ArchivingMetrics/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..8696e9c5a9f9751005f1a4f050f04ff51a6216fa --- /dev/null +++ b/plugins/ArchivingMetrics/lang/hr.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "Bilježi metrike o performansama arhiviranja." + } +} diff --git a/plugins/ArchivingMetrics/lang/hu.json b/plugins/ArchivingMetrics/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/hu.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/hy.json b/plugins/ArchivingMetrics/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/id.json b/plugins/ArchivingMetrics/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/id.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/is.json b/plugins/ArchivingMetrics/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/is.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/it.json b/plugins/ArchivingMetrics/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/it.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ja.json b/plugins/ArchivingMetrics/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ja.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ka.json b/plugins/ArchivingMetrics/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ka.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ko.json b/plugins/ArchivingMetrics/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ko.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ku.json b/plugins/ArchivingMetrics/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ku.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/lb.json b/plugins/ArchivingMetrics/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/lb.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/lt.json b/plugins/ArchivingMetrics/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/lt.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/lv.json b/plugins/ArchivingMetrics/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/lv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ms.json b/plugins/ArchivingMetrics/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ms.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/nb.json b/plugins/ArchivingMetrics/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/nb.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/nl.json b/plugins/ArchivingMetrics/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/nl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/nn.json b/plugins/ArchivingMetrics/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/nn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/pl.json b/plugins/ArchivingMetrics/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/pl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/pt-br.json b/plugins/ArchivingMetrics/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..b1ff60e0cfc9baf44c53966de5fa7abe6663f24f --- /dev/null +++ b/plugins/ArchivingMetrics/lang/pt-br.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "Registra métricas sobre o desempenho do arquivamento." + } +} diff --git a/plugins/ArchivingMetrics/lang/pt.json b/plugins/ArchivingMetrics/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/pt.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ro.json b/plugins/ArchivingMetrics/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ro.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ru.json b/plugins/ArchivingMetrics/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ru.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/si.json b/plugins/ArchivingMetrics/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/sk.json b/plugins/ArchivingMetrics/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/sk.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/sl.json b/plugins/ArchivingMetrics/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/sl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/sq.json b/plugins/ArchivingMetrics/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/sq.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/sr.json b/plugins/ArchivingMetrics/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/sr.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/sv.json b/plugins/ArchivingMetrics/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/sv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ta.json b/plugins/ArchivingMetrics/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..1086386ea5938bd339f6dd9e41b3e05a71bc81b9 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ta.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "செயல்திறனை காப்பகப்படுத்துவது பற்றிய அளவீடுகளை பதிவு செய்கிறது." + } +} diff --git a/plugins/ArchivingMetrics/lang/te.json b/plugins/ArchivingMetrics/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/te.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/th.json b/plugins/ArchivingMetrics/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/th.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/tl.json b/plugins/ArchivingMetrics/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/tl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/tr.json b/plugins/ArchivingMetrics/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..0da222e95cd3280f7a2d684c26e843a5c628991d --- /dev/null +++ b/plugins/ArchivingMetrics/lang/tr.json @@ -0,0 +1,5 @@ +{ + "ArchivingMetrics": { + "PluginDescription": "Arşivleme başarımı ile ilgili ölçümleri kaydeder." + } +} diff --git a/plugins/ArchivingMetrics/lang/tzm.json b/plugins/ArchivingMetrics/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/tzm.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/uk.json b/plugins/ArchivingMetrics/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/uk.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/ur.json b/plugins/ArchivingMetrics/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/ur.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/vi.json b/plugins/ArchivingMetrics/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/vi.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/zh-cn.json b/plugins/ArchivingMetrics/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/zh-cn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/lang/zh-tw.json b/plugins/ArchivingMetrics/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/ArchivingMetrics/lang/zh-tw.json @@ -0,0 +1 @@ +{} diff --git a/plugins/ArchivingMetrics/tests/Integration/TasksTest.php b/plugins/ArchivingMetrics/tests/Integration/TasksTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0dc3791a344fc66383a7535e1545f037dd5e074d --- /dev/null +++ b/plugins/ArchivingMetrics/tests/Integration/TasksTest.php @@ -0,0 +1,92 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics\tests\Integration; + +use Piwik\Common; +use Piwik\Config; +use Piwik\Date; +use Piwik\Db; +use Piwik\Plugins\ArchivingMetrics\Tasks; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group ArchivingMetrics + * @group ArchivingMetrics_Tasks + * @group Plugins + */ +class TasksTest extends IntegrationTestCase +{ + public function testPurgeOldMetricsDeletesRowsOlderThanRetention(): void + { + $config = Config::getInstance(); + $config->ArchivingMetrics = ['retention_days' => 30]; + + $this->insertRow(Date::now()->subDay(31)->getDatetime()); + $this->insertRow(Date::now()->subDay(5)->getDatetime()); + + $task = new Tasks(); + $task->purgeOldMetrics(); + + $count = (int) Db::fetchOne('SELECT COUNT(*) FROM ' . Common::prefixTable('archiving_metrics')); + $this->assertSame(1, $count); + } + + public function testPurgeOldMetricsDisabledKeepsRows(): void + { + $config = Config::getInstance(); + $config->ArchivingMetrics = ['retention_days' => 0]; + + $this->insertRow(Date::now()->subDay(400)->getDatetime()); + + $task = new Tasks(); + $task->purgeOldMetrics(); + + $count = (int) Db::fetchOne('SELECT COUNT(*) FROM ' . Common::prefixTable('archiving_metrics')); + $this->assertSame(1, $count); + } + + public function testPurgeMetricsForDeletedSitesRemovesOrphanedRows(): void + { + $idSite = Fixture::createWebsite('2024-01-01 00:00:00'); + + $this->insertRow(Date::now()->subDay(5)->getDatetime(), $idSite); + $this->insertRow(Date::now()->subDay(5)->getDatetime(), 9999); + + $task = new Tasks(); + $task->purgeMetricsForDeletedSites(); + + $count = (int) Db::fetchOne('SELECT COUNT(*) FROM ' . Common::prefixTable('archiving_metrics')); + $this->assertSame(1, $count); + } + + private function insertRow(string $tsStarted, int $idSite = 1): void + { + $table = Common::prefixTable('archiving_metrics'); + Db::query( + "INSERT INTO {$table} (idarchive, idsite, archive_name, date1, date2, period, ts_started, ts_finished, total_time, total_time_exclusive) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + 1, + $idSite, + 'done', + '2025-01-01', + '2025-01-01', + 1, + $tsStarted, + $tsStarted, + 123, + 100, + ] + ); + } +} diff --git a/plugins/ArchivingMetrics/tests/Integration/TimerDbTest.php b/plugins/ArchivingMetrics/tests/Integration/TimerDbTest.php new file mode 100644 index 0000000000000000000000000000000000000000..73c01bce4b1d061fb3e31bc7cefefb20cd178ae2 --- /dev/null +++ b/plugins/ArchivingMetrics/tests/Integration/TimerDbTest.php @@ -0,0 +1,57 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics\tests\Integration; + +use Piwik\Common; +use Piwik\Db; +use Piwik\Period; +use Piwik\Plugins\ArchivingMetrics\Clock\Clock; +use Piwik\Plugins\ArchivingMetrics\Context; +use Piwik\Plugins\ArchivingMetrics\Timer; +use Piwik\Plugins\ArchivingMetrics\Writer\DbWriter; +use Piwik\Segment; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group ArchivingMetrics + * @group ArchivingMetrics_TimerDb + * @group Plugins + */ +class TimerDbTest extends IntegrationTestCase +{ + public function testItWritesAndReadsFromDatabase(): void + { + $period = new Period\Day(\Piwik\Date::factory('2020-11-01')); + $segment = new Segment('', [1]); + $context = new Context(1, $period, $segment, ''); + + $timer = new Timer(true, new Clock(), new DbWriter()); + $timer->start($context); + $timer->complete($context, [999], false); + + $rows = Db::fetchAll('SELECT * FROM ' . Common::prefixTable('archiving_metrics')); + + self::assertCount(1, $rows, 'Expected archiving_metrics table to have exactly one record.'); + self::assertSame(999, (int) $rows[0]['idarchive']); + self::assertSame(1, (int) $rows[0]['idsite']); + self::assertNotEmpty($rows[0]['archive_name']); + self::assertSame('2020-11-01', $rows[0]['date1']); + self::assertSame('2020-11-01', $rows[0]['date2']); + self::assertIsNumeric($rows[0]['period']); + self::assertNotEmpty($rows[0]['ts_started']); + self::assertNotEmpty($rows[0]['ts_finished']); + self::assertIsNumeric($rows[0]['total_time']); + self::assertIsNumeric($rows[0]['total_time_exclusive']); + self::assertArrayHasKey('is_temporary', $rows[0]); + self::assertSame(0, (int) $rows[0]['is_temporary']); + } +} diff --git a/plugins/ArchivingMetrics/tests/Unit/TimerTest.php b/plugins/ArchivingMetrics/tests/Unit/TimerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..40290a96c5cd431d34a80aadaa1549d10f3e5e0d --- /dev/null +++ b/plugins/ArchivingMetrics/tests/Unit/TimerTest.php @@ -0,0 +1,616 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\ArchivingMetrics\tests\Unit; + +use PHPUnit\Framework\TestCase; +use Piwik\ArchiveProcessor\Rules; +use Piwik\Date; +use Piwik\Period\Factory; +use Piwik\Plugins\ArchivingMetrics\Clock\ClockInterface; +use Piwik\Plugins\ArchivingMetrics\Context; +use Piwik\Plugins\ArchivingMetrics\Timer; +use Piwik\Plugins\ArchivingMetrics\Writer\WriterInterface; +use Piwik\Segment; + +/** + * @group ArchivingMetrics + * @group ArchivingMetrics_Timer + * @group Plugins + */ +class TimerTest extends TestCase +{ + /** + * @dataProvider timerProvider + */ + public function testItRecordsArchivingRunsWithCorrectTiming(array $events, array $microtimes, array $expectedRecords): void + { + $writer = new InMemoryWriter(); + $clock = $this->createMock(ClockInterface::class); + $clock->method('microtime')->willReturnOnConsecutiveCalls(...$microtimes); + $timer = new Timer(true, $clock, $writer); + + foreach ($events as $event) { + $context = $this->createContext($event['context']); + if ($event['action'] === 'start') { + $timer->start($context); + continue; + } + + $timer->complete( + $context, + $event['idArchives'], + $event['cached'] + ); + } + + $this->assertSame($expectedRecords, $writer->records); + } + + public function testItSkipsWhenArchivePhpNotTriggered(): void + { + $writer = new InMemoryWriter(); + $clock = $this->createMock(ClockInterface::class); + $clock->method('microtime')->willReturnOnConsecutiveCalls(0.0); + $timer = new Timer(false, $clock, $writer); + + $context = $this->createContext([ + 'idSite' => 1, + 'segment' => '', + 'plugin' => '', + 'date1' => '2024-01-01', + 'date2' => '2024-01-01', + 'period' => 'day', + ]); + + $timer->start($context); + $timer->complete($context, [123], false); + + $this->assertSame([], $writer->records); + } + + /** + * @dataProvider isTemporaryArchiveProvider + */ + public function testItSetsIsTemporaryFlagCorrectly( + string $periodLabel, + string $date, + string $siteTimezone, + int $startTimestamp, + int $expectedIsTemporary + ): void { + $writer = new TimingCaptureWriter(); + $clock = $this->createMock(ClockInterface::class); + $clock->method('microtime')->willReturnOnConsecutiveCalls( + $startTimestamp, + $startTimestamp + 1 + ); + $timer = new Timer(true, $clock, $writer); + + $period = Factory::build($periodLabel, $date); + $segment = $this->createSegment(''); + $context = new Context(1, $period, $segment, '', false, $siteTimezone); + + $timer->start($context); + $timer->complete($context, [123], false); + + $this->assertCount(1, $writer->records); + $this->assertSame($expectedIsTemporary, $writer->records[0]['timing']['is_temporary']); + } + + public function isTemporaryArchiveProvider(): \Generator + { + yield 'day in progress is temporary' => [ + 'periodLabel' => 'day', + 'date' => '2025-11-01', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-11-01 12:00:00', 'UTC'), + 'expectedIsTemporary' => 1, + ]; + yield 'past day is not temporary' => [ + 'periodLabel' => 'day', + 'date' => '2025-10-31', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-11-01 00:00:00', 'UTC'), + 'expectedIsTemporary' => 0, + ]; + yield 'week in progress is temporary' => [ + 'periodLabel' => 'week', + 'date' => '2025-11-01', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-11-01 12:00:00', 'UTC'), + 'expectedIsTemporary' => 1, + ]; + yield 'past week is not temporary' => [ + 'periodLabel' => 'week', + 'date' => '2025-10-20', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-11-01 12:00:00', 'UTC'), + 'expectedIsTemporary' => 0, + ]; + yield 'month in progress is temporary' => [ + 'periodLabel' => 'month', + 'date' => '2025-11-01', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-11-15 12:00:00', 'UTC'), + 'expectedIsTemporary' => 1, + ]; + yield 'year in progress is temporary' => [ + 'periodLabel' => 'year', + 'date' => '2025-01-01', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-06-01 12:00:00', 'UTC'), + 'expectedIsTemporary' => 1, + ]; + yield 'range in progress is temporary' => [ + 'periodLabel' => 'range', + 'date' => '2025-11-01,2025-11-10', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-11-05 12:00:00', 'UTC'), + 'expectedIsTemporary' => 1, + ]; + yield 'exactly at period end is not temporary' => [ + 'periodLabel' => 'day', + 'date' => '2025-11-01', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-11-01 23:59:59', 'UTC'), + 'expectedIsTemporary' => 0, + ]; + yield 'one second before period end is temporary' => [ + 'periodLabel' => 'day', + 'date' => '2025-11-01', + 'siteTimezone' => 'UTC', + 'startTimestamp' => self::timestampInTimezone('2025-11-01 23:59:58', 'UTC'), + 'expectedIsTemporary' => 1, + ]; + yield 'site timezone affects temporary determination' => [ + 'periodLabel' => 'day', + 'date' => '2025-11-02', + 'siteTimezone' => 'America/Los_Angeles', + 'startTimestamp' => self::timestampInTimezone('2025-11-02 23:30:00', 'America/Los_Angeles'), + 'expectedIsTemporary' => 1, + ]; + yield 'site timezone with positive UTC offset in progress' => [ + 'periodLabel' => 'day', + 'date' => '2025-11-02', + 'siteTimezone' => 'Asia/Tokyo', + 'startTimestamp' => self::timestampInTimezone('2025-11-01 00:00:00', 'Asia/Tokyo'), + 'expectedIsTemporary' => 1, + ]; + yield 'site timezone with positive UTC offset after day end' => [ + 'periodLabel' => 'day', + 'date' => '2025-11-02', + 'siteTimezone' => 'Asia/Tokyo', + 'startTimestamp' => self::timestampInTimezone('2025-11-03 00:00:00', 'Asia/Tokyo'), + 'expectedIsTemporary' => 0, + ]; + yield 'site timezone with negative UTC offset after day end' => [ + 'periodLabel' => 'day', + 'date' => '2025-11-02', + 'siteTimezone' => 'America/Los_Angeles', + 'startTimestamp' => self::timestampInTimezone('2025-11-10 12:00:00', 'America/Los_Angeles'), + 'expectedIsTemporary' => 0, + ]; + yield 'dst start day before local day end stays temporary' => [ + 'periodLabel' => 'day', + 'date' => '2025-03-09', + 'siteTimezone' => 'America/Los_Angeles', + 'startTimestamp' => self::timestampInTimezone('2025-03-09 23:00:00', 'America/Los_Angeles'), + 'expectedIsTemporary' => 1, + ]; + yield 'dst end day clearly before local day end is temporary' => [ + 'periodLabel' => 'day', + 'date' => '2025-11-02', + 'siteTimezone' => 'America/Los_Angeles', + 'startTimestamp' => self::timestampInTimezone('2025-11-02 18:00:00', 'America/Los_Angeles'), + 'expectedIsTemporary' => 1, + ]; + } + + public function timerProvider(): array + { + // Blank segment ensures Rules::getDoneStringFlagFor returns "done" so the timer is active. + $base = [ + 'idSite' => 1, + 'segment' => '', + 'plugin' => '', + 'date1' => '2024-01-01', + 'date2' => '2024-01-01', + ]; + + return [ + 'single period' => [ + 'events' => [ + ['action' => 'start', 'context' => array_merge($base, ['period' => 'day'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'day']), 'idArchives' => [101], 'cached' => false], + ], + 'microtimes' => [ + strtotime('2024-01-01 00:00:00'), + strtotime('2024-01-01 00:00:00') + 1.2, + ], + 'expectedRecords' => [ + [ + 'idarchive' => 101, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-01', + 'date2' => '2024-01-01', + 'period' => 1, + 'ts_started' => '2024-01-01 00:00:00', + 'ts_finished' => '2024-01-01 00:00:01', + 'total_time' => 1200, + 'total_time_exclusive' => 1200, + ], + ], + ], + 'mix of events with no nesting' => [ + 'events' => [ + ['action' => 'start', 'context' => array_merge($base, ['period' => 'year', 'date1' => '2024-01-01', 'date2' => '2024-12-31'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'year', 'date1' => '2024-01-01', 'date2' => '2024-12-31']), 'idArchives' => [303], 'cached' => false], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-02-01', 'date2' => '2024-02-01'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-02-01', 'date2' => '2024-02-01']), 'idArchives' => [204], 'cached' => false], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'month', 'date1' => '2024-02-01', 'date2' => '2024-02-29'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'month', 'date1' => '2024-02-01', 'date2' => '2024-02-29']), 'idArchives' => [202], 'cached' => false], + ], + 'microtimes' => [ + strtotime('2024-01-01 00:00:00'), + strtotime('2024-01-01 00:00:00') + 6.3, + strtotime('2024-02-01 00:00:01'), + strtotime('2024-02-01 00:00:01') + 5.4, + strtotime('2024-02-01 00:00:01'), + strtotime('2024-02-01 00:00:01') + 12.3, + ], + 'expectedRecords' => [ + [ + 'idarchive' => 303, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-01', + 'date2' => '2024-12-31', + 'period' => 4, + 'ts_started' => '2024-01-01 00:00:00', + 'ts_finished' => '2024-01-01 00:00:06', + 'total_time' => 6300, + 'total_time_exclusive' => 6300, + ], + [ + 'idarchive' => 204, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-02-01', + 'date2' => '2024-02-01', + 'period' => 1, + 'ts_started' => '2024-02-01 00:00:01', + 'ts_finished' => '2024-02-01 00:00:06', + 'total_time' => 5400, + 'total_time_exclusive' => 5400, + ], + [ + 'idarchive' => 202, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-02-01', + 'date2' => '2024-02-29', + 'period' => 3, + 'ts_started' => '2024-02-01 00:00:01', + 'ts_finished' => '2024-02-01 00:00:13', + 'total_time' => 12300, + 'total_time_exclusive' => 12300, + ], + ], + ], + 'mix of events with no nesting and some fetched from cache' => [ + 'events' => [ + ['action' => 'start', 'context' => array_merge($base, ['period' => 'year', 'date1' => '2024-01-01', 'date2' => '2024-12-31'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'year', 'date1' => '2024-01-01', 'date2' => '2024-12-31']), 'idArchives' => [303], 'cached' => true], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-02-01', 'date2' => '2024-02-01'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-02-01', 'date2' => '2024-02-01']), 'idArchives' => [204], 'cached' => false], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'month', 'date1' => '2024-02-01', 'date2' => '2024-02-29'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'month', 'date1' => '2024-02-01', 'date2' => '2024-02-29']), 'idArchives' => [202], 'cached' => true], + ], + 'microtimes' => [ + strtotime('2024-01-01 00:00:00'), + strtotime('2024-02-01 00:00:00'), + strtotime('2024-02-01 00:00:00') + 3.0, + strtotime('2024-02-01 00:00:01'), + ], + 'expectedRecords' => [ + [ + 'idarchive' => 204, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-02-01', + 'date2' => '2024-02-01', + 'period' => 1, + 'ts_started' => '2024-02-01 00:00:00', + 'ts_finished' => '2024-02-01 00:00:03', + 'total_time' => 3000, + 'total_time_exclusive' => 3000, + ], + ], + ], + 'nested day inside week' => [ + 'events' => [ + ['action' => 'start', 'context' => array_merge($base, ['period' => 'week', 'date1' => '2024-01-08', 'date2' => '2024-01-14'])], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-01-10', 'date2' => '2024-01-10'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-01-10', 'date2' => '2024-01-10']), 'idArchives' => [202], 'cached' => false], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'week', 'date1' => '2024-01-08', 'date2' => '2024-01-14']), 'idArchives' => [303], 'cached' => false], + ], + 'microtimes' => [ + strtotime('2024-01-01 00:00:00'), + strtotime('2024-01-01 00:00:00') + 0.5, + strtotime('2024-01-01 00:00:00') + 1.1, + strtotime('2024-01-01 00:00:00') + 2.5, + ], + 'expectedRecords' => [ + [ + 'idarchive' => 202, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-10', + 'date2' => '2024-01-10', + 'period' => 1, + 'ts_started' => '2024-01-01 00:00:00', + 'ts_finished' => '2024-01-01 00:00:01', + 'total_time' => 600, + 'total_time_exclusive' => 600, + ], + [ + 'idarchive' => 303, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-08', + 'date2' => '2024-01-14', + 'period' => 2, + 'ts_started' => '2024-01-01 00:00:00', + 'ts_finished' => '2024-01-01 00:00:02', + 'total_time' => 2500, + 'total_time_exclusive' => 1900, + ], + ], + ], + 'full cascade year <-> day' => [ + 'events' => [ + ['action' => 'start', 'context' => array_merge($base, ['period' => 'year', 'date1' => '2024-01-01', 'date2' => '2024-12-31'])], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'month', 'date1' => '2024-01-01', 'date2' => '2024-01-31'])], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'week', 'date1' => '2024-01-08', 'date2' => '2024-01-14'])], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-01-10', 'date2' => '2024-01-10'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-01-10', 'date2' => '2024-01-10']), 'idArchives' => [202], 'cached' => false], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'week', 'date1' => '2024-01-08', 'date2' => '2024-01-14']), 'idArchives' => [303], 'cached' => false], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'week', 'date1' => '2024-01-15', 'date2' => '2024-01-22'])], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-01-20', 'date2' => '2024-01-20'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-01-20', 'date2' => '2024-01-20']), 'idArchives' => [404], 'cached' => false], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'week', 'date1' => '2024-01-15', 'date2' => '2024-01-21']), 'idArchives' => [505], 'cached' => false], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'month', 'date1' => '2024-01-01', 'date2' => '2024-01-31']), 'idArchives' => [606], 'cached' => false], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'year', 'date1' => '2024-01-01', 'date2' => '2024-12-31']), 'idArchives' => [707], 'cached' => false], + ], + 'microtimes' => [ + strtotime('2024-01-01 00:00:00'), + strtotime('2024-01-01 00:00:00') + 1, + strtotime('2024-01-01 00:00:00') + 2, + strtotime('2024-01-01 00:00:00') + 4, + strtotime('2024-01-01 00:00:00') + 8, + strtotime('2024-01-01 00:00:00') + 16, + strtotime('2024-01-01 00:00:00') + 32, + strtotime('2024-01-01 00:00:00') + 64, + strtotime('2024-01-01 00:00:00') + 128, + strtotime('2024-01-01 00:00:00') + 256, + strtotime('2024-01-01 00:00:00') + 512, + strtotime('2024-01-01 00:00:00') + 1024, + ], + 'expectedRecords' => [ + [ + 'idarchive' => 202, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-10', + 'date2' => '2024-01-10', + 'period' => 1, + 'ts_started' => '2024-01-01 00:00:04', + 'ts_finished' => '2024-01-01 00:00:08', + 'total_time' => 4000, + 'total_time_exclusive' => 4000, + ], + [ + 'idarchive' => 303, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-08', + 'date2' => '2024-01-14', + 'period' => 2, + 'ts_started' => '2024-01-01 00:00:02', + 'ts_finished' => '2024-01-01 00:00:16', + 'total_time' => 14000, + 'total_time_exclusive' => 10000, + ], + [ + 'idarchive' => 404, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-20', + 'date2' => '2024-01-20', + 'period' => 1, + 'ts_started' => '2024-01-01 00:01:04', + 'ts_finished' => '2024-01-01 00:02:08', + 'total_time' => 64000, + 'total_time_exclusive' => 64000, + ], + [ + 'idarchive' => 505, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-15', + 'date2' => '2024-01-21', + 'period' => 2, + 'ts_started' => '2024-01-01 00:00:32', + 'ts_finished' => '2024-01-01 00:04:16', + 'total_time' => 224000, + 'total_time_exclusive' => 160000, + ], + [ + 'idarchive' => 606, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-01', + 'date2' => '2024-01-31', + 'period' => 3, + 'ts_started' => '2024-01-01 00:00:01', + 'ts_finished' => '2024-01-01 00:08:32', + 'total_time' => 511000, + 'total_time_exclusive' => 273000, + ], + [ + 'idarchive' => 707, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-01-01', + 'date2' => '2024-12-31', + 'period' => 4, + 'ts_started' => '2024-01-01 00:00:00', + 'ts_finished' => '2024-01-01 00:17:04', + 'total_time' => 1024000, + 'total_time_exclusive' => 513000, + ], + ], + ], + 'segments have timings recorded' => [ + 'events' => [ + ['action' => 'start', 'context' => array_merge($base, ['period' => 'year', 'date1' => '2024-01-01', 'date2' => '2024-12-31', 'segment' => 'browserCode==FF'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'year', 'date1' => '2024-01-01', 'date2' => '2024-12-31', 'segment' => 'browserCode==FF']), 'idArchives' => [303], 'cached' => false], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-02-01', 'date2' => '2024-02-01'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'day', 'date1' => '2024-02-01', 'date2' => '2024-02-01']), 'idArchives' => [204], 'cached' => false], + ['action' => 'start', 'context' => array_merge($base, ['period' => 'month', 'date1' => '2024-02-01', 'date2' => '2024-02-29'])], + ['action' => 'complete', 'context' => array_merge($base, ['period' => 'month', 'date1' => '2024-02-01', 'date2' => '2024-02-29']), 'idArchives' => [202], 'cached' => false], + ], + 'microtimes' => [ + strtotime('2024-01-01 00:00:00'), + strtotime('2024-01-01 00:00:00') + 6.3, + strtotime('2024-02-01 00:00:01'), + strtotime('2024-02-01 00:00:01') + 5.4, + strtotime('2024-02-01 00:00:01'), + strtotime('2024-02-01 00:00:01') + 12.3, + ], + 'expectedRecords' => [ + [ + 'idarchive' => 303, + 'idsite' => 1, + 'archive_name' => 'done' . md5('browserCode==FF'), + 'date1' => '2024-01-01', + 'date2' => '2024-12-31', + 'period' => 4, + 'ts_started' => '2024-01-01 00:00:00', + 'ts_finished' => '2024-01-01 00:00:06', + 'total_time' => 6300, + 'total_time_exclusive' => 6300, + ], + [ + 'idarchive' => 204, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-02-01', + 'date2' => '2024-02-01', + 'period' => 1, + 'ts_started' => '2024-02-01 00:00:01', + 'ts_finished' => '2024-02-01 00:00:06', + 'total_time' => 5400, + 'total_time_exclusive' => 5400, + ], + [ + 'idarchive' => 202, + 'idsite' => 1, + 'archive_name' => 'done', + 'date1' => '2024-02-01', + 'date2' => '2024-02-29', + 'period' => 3, + 'ts_started' => '2024-02-01 00:00:01', + 'ts_finished' => '2024-02-01 00:00:13', + 'total_time' => 12300, + 'total_time_exclusive' => 12300, + ], + ], + ], + ]; + } + + private function createContext(array $data): Context + { + $period = Factory::build($data['period'], $data['date1']); + + $segment = $this->createSegment($data['segment']); + + return new Context( + $data['idSite'], + $period, + $segment, + $data['plugin'], + $data['report'] ?? false + ); + } + private function createSegment(string $segmentString): Segment + { + $segment = $this->createMock(Segment::class); + $segment->method('getString')->willReturn($segmentString); + $segment->method('getHash')->willReturn($segmentString === '' ? '' : md5(urldecode($segmentString))); + $segment->method('isEmpty')->willReturn($segmentString === ''); + return $segment; + } + + private static function timestampInTimezone(string $datetime, string $timezone): int + { + return Date::factory($datetime, $timezone)->getTimestampUTC(); + } +} + +class InMemoryWriter implements WriterInterface +{ + public $records = []; + + public function write(Context $context, array $timing): void + { + $baseTiming = [ + 'idarchive' => $timing['idarchive'], + 'ts_started' => $timing['ts_started'], + 'ts_finished' => $timing['ts_finished'], + 'total_time' => $timing['total_time'], + 'total_time_exclusive' => $timing['total_time_exclusive'], + ]; + + $this->records[] = array_merge( + [ + 'idarchive' => $baseTiming['idarchive'], + 'idsite' => $context->idSite, + 'archive_name' => Rules::getDoneStringFlagFor( + [$context->idSite], + $context->segment, + $context->period->getLabel(), + $context->plugin + ), + 'date1' => $context->period->getDateTimeStart()->toString('Y-m-d'), + 'date2' => $context->period->getDateTimeEnd()->toString('Y-m-d'), + 'period' => $context->period->getId(), + ], + $baseTiming + ); + } +} + +class TimingCaptureWriter implements WriterInterface +{ + public $records = []; + + public function write(Context $context, array $timing): void + { + $this->records[] = [ + 'context' => $context, + 'timing' => $timing, + ]; + } +} diff --git a/plugins/Bandwidth/.gitattributes b/plugins/Bandwidth/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..d6be3a7c5c8b4d8096ac6f5a2cbdf08284bae50a --- /dev/null +++ b/plugins/Bandwidth/.gitattributes @@ -0,0 +1,6 @@ +# Files excluded from git packages +tests/ export-ignore +.github/ export-ignore +.gitignore export-ignore +.gitattributes export-ignore +.travis.yml export-ignore diff --git a/plugins/Bandwidth/.github/workflows/matomo-ai-checklist.yml b/plugins/Bandwidth/.github/workflows/matomo-ai-checklist.yml new file mode 100644 index 0000000000000000000000000000000000000000..cc2c022da6f944b9683668c262330ca060d15f8b --- /dev/null +++ b/plugins/Bandwidth/.github/workflows/matomo-ai-checklist.yml @@ -0,0 +1,32 @@ +# Action for running tests + +name: AI Checklist + +on: + pull_request: + types: [opened, synchronize, reopened, edited] + +permissions: + actions: read + checks: none + contents: none + deployments: none + issues: none + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: none + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + AiChecklist: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + steps: + - name: Run tests + uses: matomo-org/github-action-checklist-gate@main diff --git a/plugins/Bandwidth/.github/workflows/matomo-tests.yml b/plugins/Bandwidth/.github/workflows/matomo-tests.yml new file mode 100644 index 0000000000000000000000000000000000000000..5796b3e3e11647553288acb875a66dd1c7c1bc0a --- /dev/null +++ b/plugins/Bandwidth/.github/workflows/matomo-tests.yml @@ -0,0 +1,74 @@ +# Action for running tests +# This file has been automatically created. +# To recreate it you can run this command +# ./console generate:test-action --plugin="Bandwidth" --php-versions="matomo5_min_php,matomo5_max_php" --schedule-cron="10 2 * * 6" + +name: Plugin Bandwidth Tests + +on: + pull_request: + types: [opened, synchronize] + push: + branches: + - '**.x-dev' + workflow_dispatch: + schedule: + - cron: "10 2 * * 6" + +permissions: + actions: read + checks: none + contents: read + deployments: none + issues: read + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: none + +concurrency: + group: php-${{ github.ref }} + cancel-in-progress: true + +jobs: + PluginTests: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + php: [ 'matomo5_min_php', 'matomo5_max_php' ] + target: ['minimum_required_matomo', 'maximum_supported_matomo'] + steps: + - uses: actions/checkout@v3 + with: + lfs: true + persist-credentials: false + - name: Install package ripgrep + run: sudo apt-get install ripgrep + - name: Run tests + uses: matomo-org/github-action-tests@main + with: + plugin-name: 'Bandwidth' + php-version: ${{ matrix.php }} + test-type: 'PluginTests' + matomo-test-branch: ${{ matrix.target }} + artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} + upload-artifacts: ${{ matrix.php == 'matomo5_min_php' && matrix.target == 'maximum_supported_matomo' }} + UI: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + with: + lfs: true + persist-credentials: false + - name: running tests + uses: matomo-org/github-action-tests@main + with: + plugin-name: 'Bandwidth' + matomo-test-branch: 'maximum_supported_matomo' + test-type: 'UI' + php-version: 'matomo5_min_php' + node-version: '16' + artifacts-pass: ${{ secrets.ARTIFACTS_PASS }} + upload-artifacts: true diff --git a/plugins/Bandwidth/.github/workflows/phpcs.yml b/plugins/Bandwidth/.github/workflows/phpcs.yml new file mode 100644 index 0000000000000000000000000000000000000000..dd02e7e6672d401d60235d41ab99fb4e40e9a0bc --- /dev/null +++ b/plugins/Bandwidth/.github/workflows/phpcs.yml @@ -0,0 +1,43 @@ +name: PHPCS check + +on: pull_request + +permissions: + actions: read + checks: read + contents: read + deployments: none + issues: read + packages: none + pull-requests: read + repository-projects: none + security-events: none + statuses: read + +jobs: + phpcs: + name: PHPCS + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + lfs: false + persist-credentials: false + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + tools: cs2pr + - name: Install dependencies + run: + composer init --name=matomo/bandwidth --quiet; + composer --no-plugins config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true -n; + composer config repositories.matomo-coding-standards vcs https://github.com/matomo-org/matomo-coding-standards -n; + composer require matomo-org/matomo-coding-standards:dev-master; + composer install --dev --prefer-dist --no-progress --no-suggest + - name: Check PHP code styles + id: phpcs + run: ./vendor/bin/phpcs --report-full --standard=phpcs.xml --report-checkstyle=./phpcs-report.xml + - name: Show PHPCS results in PR + if: ${{ always() && steps.phpcs.outcome == 'failure' }} + run: cs2pr ./phpcs-report.xml --prepend-filename diff --git a/plugins/Bandwidth/.gitignore b/plugins/Bandwidth/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3a2568fee661b7df1ad64314b1c757b27001be51 --- /dev/null +++ b/plugins/Bandwidth/.gitignore @@ -0,0 +1,2 @@ +tests/UI/processed-ui-screenshots +tests/UI/screenshot-diffs \ No newline at end of file diff --git a/plugins/Bandwidth/API.php b/plugins/Bandwidth/API.php new file mode 100644 index 0000000000000000000000000000000000000000..bb018e49602dae714d18dae4d5e385ab191e8475 --- /dev/null +++ b/plugins/Bandwidth/API.php @@ -0,0 +1,71 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth; + +use Piwik\Archive; +use Piwik\DataTable; +use Piwik\Piwik; + +/** + * Exposes reporting API endpoints for aggregated bandwidth metrics. + * + * @method static \Piwik\Plugins\Bandwidth\API getInstance() + */ +class API extends \Piwik\Plugin\API +{ + /** + * Returns aggregated bandwidth metrics for the requested site selection and archive period. + * Includes overall, pageview, and download totals, optionally limited to specific metric columns. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|null|false $segment Custom segment to filter the report. + * Example: "referrerName==example.com" + * Supports AND (;) and OR (,) operators. + * @param string|array|false $columns Metric columns to return. + * Accepts a comma-separated list, an array of metric names, + * or false to return all available bandwidth metrics. + * @return DataTable|DataTable\Map A table containing the requested bandwidth metric totals as integers. + */ + public function get($idSite, $period, $date, $segment = false, $columns = false) + { + Piwik::checkUserHasViewAccess($idSite); + + $archive = Archive::build($idSite, $period, $date, $segment); + + $columnNames = Metrics::getNumericRecordNameToColumnsMapping(); + $archiveRecordNames = array_keys($columnNames); + $metricColumnNames = array_values($columnNames); + + $dataTable = $archive->getDataTableFromNumeric($archiveRecordNames); + $dataTable->filter('ReplaceColumnNames', [$columnNames]); + $dataTable->filter(function (DataTable $dataTable) use ($metricColumnNames) { + foreach ($dataTable->getRows() as $row) { + foreach ($metricColumnNames as $metric) { + $row->setColumn($metric, (int)$row->getColumn($metric)); + } + } + }); + + $requestedColumns = Piwik::getArrayFromApiParameter($columns); + $columnsToShow = $requestedColumns ?: $metricColumnNames; + $dataTable->queueFilter('ColumnDelete', [$columnsToRemove = [], $columnsToShow]); + + return $dataTable; + } +} diff --git a/plugins/Bandwidth/Archiver.php b/plugins/Bandwidth/Archiver.php new file mode 100644 index 0000000000000000000000000000000000000000..dc470065fbeed4fce64fe60481e5f7302315906d --- /dev/null +++ b/plugins/Bandwidth/Archiver.php @@ -0,0 +1,21 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth; + +/** + * + */ +class Archiver extends \Piwik\Plugin\Archiver +{ + public const BANDWIDTH_TOTAL_RECORD = "Bandwidth_nb_total_overall"; + public const BANDWIDTH_PAGEVIEW_RECORD = "Bandwidth_nb_total_pageurl"; + public const BANDWIDTH_DOWNLOAD_RECORD = "Bandwidth_nb_total_download"; +} diff --git a/plugins/Bandwidth/Bandwidth.php b/plugins/Bandwidth/Bandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..5a94dbeefdc0dbabda62e99638bc701025dd5b6b --- /dev/null +++ b/plugins/Bandwidth/Bandwidth.php @@ -0,0 +1,216 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth; + +use Piwik\Common; +use Piwik\DataTable; +use Piwik\FrontController; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Bandwidth\Columns\Bandwidth as BandwidthColumn; +use Piwik\Plugins\CoreVisualizations\Metrics\Formatter\Numeric; +use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines; + +class Bandwidth extends \Piwik\Plugin +{ + // module => action. The ones that are defined here will be enriched by bandwidth columns when displayed in the UI + private $reportsToEnrich = [ + 'Actions' => [ + 'getPageUrls', + 'getPageUrl', + 'getPageTitles', + 'getPageTitle', + 'getDownloads', + 'getDownload', + 'getOutlink', + 'getOutlinks', + 'getEntryPageTitles', + 'getEntryPageUrls', + 'getExitPageTitles', + 'getExitPageUrls', + 'getSiteSearchKeywords', + 'getSiteSearchNoResultKeywords', + 'getPageTitlesFollowingSiteSearch', + 'getPageUrlsFollowingSiteSearch', + ], + 'CustomDimensions' => [ + 'getCustomDimension', + ], + ]; + + // we will only show columns in that report in the UI if there was at least one byte tracked for the defined metric + private $enrichReportIfTotalHasValue = [ + 'Actions.getPageUrls' => Metrics::COLUMN_TOTAL_PAGEVIEW_BANDWIDTH, + 'Actions.getPageTitles' => Metrics::COLUMN_TOTAL_PAGEVIEW_BANDWIDTH, + 'Actions.getDownloads' => Metrics::COLUMN_TOTAL_DOWNLOAD_BANDWIDTH, + '*' => Metrics::COLUMN_TOTAL_OVERALL_BANDWIDTH // for all other reports use this + ]; + + /** + * @see \Piwik\Plugin::registerEvents + */ + public function registerEvents() + { + $hooks = [ + 'ViewDataTable.configure' => 'configureViewDataTable', + 'Actions.Archiving.addActionMetrics' => 'addActionMetrics', + 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations', + 'Actions.getCustomActionDimensionFieldsAndJoins' => 'provideActionDimensionFields', + 'Metrics.getDefaultMetricSemanticTypes' => 'addMetricSemanticTypes', + 'Metrics.addMetricIdToNameMapping' => 'addMetricIdToNameMapping', + 'Metrics.getEvolutionUnit' => 'getEvolutionUnit', + ]; + + foreach ($this->reportsToEnrich as $module => $actions) { + foreach ($actions as $action) { + $hooks['API.' . $module . '.' . $action . '.end'] = 'enrichApi'; + } + } + + return $hooks; + } + + public function renderSparklines(&$out) + { + $out .= FrontController::getInstance()->dispatch('Bandwidth', 'sparklines'); + } + + public function addMetricTranslations(&$translations) + { + $metrics = Metrics::getMetricTranslations(); + $translations = array_merge($translations, $metrics); + } + + public function addMetricSemanticTypes(array &$types): void + { + $metricTypes = Metrics::getMetricSemanticTypes(); + $types = array_merge($types, $metricTypes); + } + + public function getEvolutionUnit(&$unit, $column, $idSite) + { + if (!property_exists(Numeric::class, 'byteSizeUnit')) { + // don't use a default unit in Matomo versions where it didn't exist yet + return; + } + + foreach (Metrics::getBandwidthMetrics() as $metric) { + if ($metric->getName() === $column) { + $unit = ' ' . Numeric::$byteSizeUnit; + return; + } + } + foreach (Metrics::getOverallMetrics() as $metric) { + if ($metric->getName() === $column) { + $unit = ' ' . Numeric::$byteSizeUnit; + return; + } + } + } + + public function addActionMetrics(&$metricsConfig) + { + foreach (Metrics::getActionMetrics() as $metric => $config) { + $metricsConfig[$metric] = $config; + } + } + + public function addMetricIdToNameMapping(&$mapping) + { + foreach (Metrics::getBandwidthMetrics() as $metric) { + $metricId = $metric->getMetricId(); + if (!is_int($metricId)) { + continue; + } + $mapping[$metricId] = $metric->getName(); + } + } + + public function configureViewDataTable(ViewDataTable $view) + { + $module = $view->requestConfig->getApiModuleToRequest(); + $method = $view->requestConfig->getApiMethodToRequest(); + + if ($module === 'API' && $method === 'get' && property_exists($view->config, 'selectable_columns')) { + // here we want to make sure the total column is selectable + $selectable = $view->config->selectable_columns ?: []; + $columns = array_values(Metrics::getNumericRecordNameToColumnsMapping()); + + $view->config->selectable_columns = array_merge($selectable, $columns); + $view->config->addTranslations(Metrics::getMetricTranslations()); + } + + if ($module === 'API' && $method === 'get' && $view->isViewDataTableId(Sparklines::ID)) { + // TODO: should use a metric class or something so we don't have to manually support comparisons here + + /** @var Sparklines $view */ + $view->config->addSparklineMetric(Metrics::COLUMN_TOTAL_OVERALL_BANDWIDTH); + } + + if (array_key_exists($module, $this->reportsToEnrich) && in_array($method, $this->reportsToEnrich[$module])) { + $idSite = Common::getRequestVar('idSite'); + $date = Common::getRequestVar('date'); + $period = Common::getRequestVar('period', 'month', 'string'); + + if (array_key_exists($module . '.' . $method, $this->enrichReportIfTotalHasValue)) { + $columnToCompare = $this->enrichReportIfTotalHasValue[$module . '.' . $method]; + } else { + $columnToCompare = $this->enrichReportIfTotalHasValue['*']; + } + + $bandwidthDimension = new BandwidthColumn(); + $isUsed = $bandwidthDimension->isUsedInSite($idSite, $period, $date, $columnToCompare); + + if (!$isUsed) { + return; + } + + $view->config->columns_to_display[] = 'avg_bandwidth'; + $view->config->columns_to_display[] = 'sum_bandwidth'; + $view->config->addTranslations(Metrics::getMetricTranslations()); + } + } + + public function enrichApi(DataTable\DataTableInterface $dataTable, $params) + { + $dataTable->filter(function (DataTable $dataTable) { + $extraProcessedMetrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME); + + if (empty($extraProcessedMetrics)) { + $extraProcessedMetrics = []; + } + + foreach (Metrics::getBandwidthMetrics() as $metric) { + $extraProcessedMetrics[] = $metric; + } + foreach (Metrics::getOverallMetrics() as $metric) { + $extraProcessedMetrics[] = $metric; + } + $dataTable->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, $extraProcessedMetrics); + }); + + $dataTable->filter(function (DataTable $dataTable) { + $metricIdsToName = []; + foreach (Metrics::getBandwidthMetrics() as $metric) { + $metricId = $metric->getMetricId(); + if (!empty($metricId)) { + $metricIdsToName[$metricId] = $metric->getName(); + } + } + $dataTable->queueFilter('ReplaceColumnNames', [$metricIdsToName]); + }); + } + + public function provideActionDimensionFields(&$fields, &$joins) + { + $column = new BandwidthColumn(); + $fields[] = $column->getColumnName(); + } +} diff --git a/plugins/Bandwidth/CHANGELOG.md b/plugins/Bandwidth/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..443174700d630a8e62608e5cad596fc54442cbaf --- /dev/null +++ b/plugins/Bandwidth/CHANGELOG.md @@ -0,0 +1,18 @@ +## Changelog + +- 5.0.6 - 2026-04-27 - Updated API documentation +- 5.0.5 - 2026-03-02 - Updated API documentation +- 5.0.4 - 2025-10-27 - Updated screenshot for Marketplace +- 5.0.3 - 2025-07-07 - Textual change +- 5.0.2 Added plugin category for Marketplace +- 5.0.1 Compatibility with Matomo 5.0.0-b4 +- 5.0.0 Compatibility with Matomo 5.0 +- 4.0.5 Reused Dimension TYPE_ constants +- 4.0.4 Added metric semantics type +- 4.0.3 Updated expected test files +- 4.0.2 Added support for showing evolution values on sparklines +- 4.0.1 Compatibility with Matomo 4.0 +- 4.0.0 Compatibility with Matomo 4.0 +- 3.2.0 Support for comparisons view. +- 3.0.0 Compatibility with Piwik 3.0 +- 0.1.0 Initial Release diff --git a/plugins/Bandwidth/Columns/Bandwidth.php b/plugins/Bandwidth/Columns/Bandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..84aa4a3feea129e840b5bdfe4bc7fa1922ff6f4c --- /dev/null +++ b/plugins/Bandwidth/Columns/Bandwidth.php @@ -0,0 +1,120 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth\Columns; + +use Piwik\Columns\DimensionSegmentFactory; +use Piwik\Common; +use Piwik\Period\Range; +use Piwik\Piwik; +use Piwik\Plugin\Dimension\ActionDimension; +use Piwik\Plugin\Segment; +use Piwik\Plugins\Bandwidth\API as BandwidthApi; +use Piwik\Segment\SegmentsList; +use Piwik\Tracker\Action; +use Piwik\Tracker\Request; +use Piwik\Tracker\Visitor; + +/** + * This example dimension recognizes a new tracking url parameter that is supposed to save the keywords that were used + * on a certain page. Please note that dimension instances are usually cached during one tracking request so they + * should be stateless (meaning an instance of this dimension will be reused if requested multiple times). + * + * See {@link http://developer.piwik.org/api-reference/Piwik/Plugin\Dimension\ActionDimension} for more information. + */ +class Bandwidth extends ActionDimension +{ + /** + * This will be the name of the column in the log_link_visit_action table if a $columnType is specified. + * + * @var string + */ + protected $columnName = 'bandwidth'; + + protected $type = self::TYPE_BYTE; + + /** + * If a columnType is defined, we will create this a column in the MySQL table having this type. Please make sure + * MySQL will understand this type. Once you change the column type the Piwik platform will notify the user to + * perform an update which can sometimes take a long time so be careful when choosing the correct column type. + * + * @var string + */ + protected $columnType = 'BIGINT(15) UNSIGNED DEFAULT NULL'; + + /** + * The name of the dimension which will be visible for instance in the UI of a related report and in the mobile app. + * + * @return string + */ + public function getName() + { + return Piwik::translate('Bandwidth_Bandwidth'); + } + + /** + * By defining one or multiple segments a user will be able to filter their visitors by this column. For instance + * show all actions only considering users having more than 10 achievement points. If you do not want to define a + * segment for this dimension just remove the column. + */ + public function configureSegments(SegmentsList $segmentsList, DimensionSegmentFactory $dimensionSegmentFactory) + { + $segment = new Segment(); + $segment->setSegment('bandwidth'); + $segment->setType(Segment::TYPE_METRIC); + $segment->setCategory('General_Actions'); + $segment->setName('Bandwidth_Bandwidth'); + $segment->setAcceptedValues('Any number in bytes, eg. 1000'); + $segmentsList->addSegment($dimensionSegmentFactory->createSegment($segment)); + } + + /** + * This event is triggered before a new action is logged to the log_link_visit_action table. It overwrites any + * looked up action so it makes usually no sense to implement both methods but it sometimes does. You can assign + * any value to the column or return boolan false in case you do not want to save any value. + * + * @param Request $request + * @param Visitor $visitor + * @param Action $action + * + * @return mixed|false + */ + public function onNewAction(Request $request, Visitor $visitor, Action $action) + { + $value = Common::getRequestVar('bw_bytes', false, 'string', $request->getParams()); + + if (is_numeric($value)) { + return (int)$value; + } + + return false; + } + + public function isUsedInSite($idSite, $period, $date, $columnToCompare) + { + if ($period === 'day' || $period === 'week') { + $period = 'month'; + } + + if (Range::isMultiplePeriod($date, $period)) { + $period = 'range'; + } + + $result = BandwidthApi::getInstance()->get($idSite, $period, $date); + + if (!$result->getRowsCount()) { + return false; + } + + $value = $result->getFirstRow()->getColumn($columnToCompare); + + return !empty($value); + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/AvgBandwidth.php b/plugins/Bandwidth/Columns/Metrics/AvgBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..3905b00692b5345dc3d3050a0484a85660c95680 --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/AvgBandwidth.php @@ -0,0 +1,66 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\DataTable\Row; +use Piwik\Piwik; +use Piwik\Columns\Dimension; +use Piwik\Plugins\Bandwidth\Metrics; + +/** + * The average amount bandwidth per page. + */ +class AvgBandwidth extends Base +{ + public function getName() + { + return 'avg_bandwidth'; + } + + public function getTranslatedName() + { + return Piwik::translate('Bandwidth_ColumnAvgBandwidth'); + } + + public function compute(Row $row) + { + // NOTE: Fetching columns by name or id is required in some cases (like flattened reports) + // This should be changed in the future: see https://github.com/piwik/piwik/issues/10916 + if ($row->hasColumn('nb_hits_with_bandwidth')) { + $hits = $this->getMetricAsIntSafe($row, 'nb_hits_with_bandwidth'); + } else { + $hits = $this->getMetricAsIntSafe($row, Metrics::METRICS_NB_HITS_WITH_BANDWIDTH); + } + + if ($row->hasColumn('sum_bandwidth')) { + $sum = $this->getMetricAsIntSafe($row, 'sum_bandwidth'); + } else { + $sum = $this->getMetricAsIntSafe($row, Metrics::METRICS_PAGE_SUM_BANDWIDTH); + } + + if (!empty($hits) && !empty($sum)) { + $avg = floor($sum / $hits); + } else { + $avg = 0; + } + + return (int)$avg; + } + + public function getDependentMetrics() + { + return ['nb_hits_with_bandwidth', 'sum_bandwidth']; + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/Base.php b/plugins/Bandwidth/Columns/Metrics/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..f4b16e49fde9ce09fad23f8e695e4b7f4d7fd157 --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/Base.php @@ -0,0 +1,69 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\DataTable\Row; +use Piwik\Metrics\Formatter; +use Piwik\Plugin\ProcessedMetric; + +abstract class Base extends ProcessedMetric +{ + protected $metric; + + public function compute(Row $row) + { + if ($this->metric) { + return $this->getMetricAsIntSafe($row, $this->metric); + } + } + + public function getTemporaryMetrics() + { + if ($this->metric) { + return [$this->metric]; + } + + return []; + } + + public function getMetricAsIntSafe(Row $row, $metric) + { + $value = $this->getMetric($row, $metric); + + if (false !== $value) { + if (strlen((string)$value) >= 9 && is_numeric($value)) { + return $value; + } else { + $value = (int)$value; + } + } + + return $value; + } + + public function format($value, Formatter $formatter) + { + if ($value) { + $value = $formatter->getPrettySizeFromBytes($value, null, 2); + } + + return $value; + } + + public function getDependentMetrics() + { + return []; + } + + public function getMetricId() + { + return $this->metric; + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/DownloadBandwidth.php b/plugins/Bandwidth/Columns/Metrics/DownloadBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..437d64b52ec70144543dc6892828f6453e788c35 --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/DownloadBandwidth.php @@ -0,0 +1,61 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\DataTable\Row; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\ProcessedMetric; + +/** + * The total amount bandwidth used for downloads. + * + * Note: This Metric should actually inherit from AggregatedMetric class, but we need to use ProcessedMetric as core + * won't format the values automatically otherwise + */ +class DownloadBandwidth extends ProcessedMetric +{ + public function getName() + { + return 'nb_total_download_bandwidth'; + } + + public function getDependentMetrics() + { + return [ + $this->getName() + ]; + } + + public function compute(Row $row) + { + return $row->getColumn($this->getName()); + } + + public function getTranslatedName() + { + return Piwik::translate('Bandwidth_ColumnTotalDownloadBandwidth'); + } + + public function format($value, Formatter $formatter) + { + if ($value) { + $value = $formatter->getPrettySizeFromBytes($value, null, 2); + } + + return $value; + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/HitsWithBandwidth.php b/plugins/Bandwidth/Columns/Metrics/HitsWithBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..e80e231ca39f57e5a238448f06905cca37d19210 --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/HitsWithBandwidth.php @@ -0,0 +1,37 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Piwik; +use Piwik\Plugins\Bandwidth\Metrics; + +/** + * The amount of a pages that were tracked having a bandwidth. + */ +class HitsWithBandwidth extends Base +{ + protected $metric = Metrics::METRICS_NB_HITS_WITH_BANDWIDTH; + + public function getName() + { + return 'nb_hits_with_bandwidth'; + } + + public function getTranslatedName() + { + return Piwik::translate('Bandwidth_ColumnHitsWithBandwidth'); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/MaxBandwidth.php b/plugins/Bandwidth/Columns/Metrics/MaxBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..e922baf0c1f3b84858026057b2bf0f152b618c19 --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/MaxBandwidth.php @@ -0,0 +1,37 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\Piwik; +use Piwik\Columns\Dimension; +use Piwik\Plugins\Bandwidth\Metrics; + +/** + * The max amount bandwidth of a pages. + */ +class MaxBandwidth extends Base +{ + protected $metric = Metrics::METRICS_PAGE_MAX_BANDWIDTH; + + public function getName() + { + return 'max_bandwidth'; + } + + public function getTranslatedName() + { + return Piwik::translate('Bandwidth_ColumnMaxBandwidth'); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/MinBandwidth.php b/plugins/Bandwidth/Columns/Metrics/MinBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..5cbed45b034fde4391f81b819d35b42f93e8fa6f --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/MinBandwidth.php @@ -0,0 +1,37 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\Piwik; +use Piwik\Columns\Dimension; +use Piwik\Plugins\Bandwidth\Metrics; + +/** + * The min amount bandwidth of a pages. + */ +class MinBandwidth extends Base +{ + protected $metric = Metrics::METRICS_PAGE_MIN_BANDWIDTH; + + public function getName() + { + return 'min_bandwidth'; + } + + public function getTranslatedName() + { + return Piwik::translate('Bandwidth_ColumnMinBandwidth'); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/OverallBandwidth.php b/plugins/Bandwidth/Columns/Metrics/OverallBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..6ae0d2fda0a6474629bceb80456dd5437ef7e0a0 --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/OverallBandwidth.php @@ -0,0 +1,61 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\DataTable\Row; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Columns\Dimension; +use Piwik\Plugin\ProcessedMetric; + +/** + * The total amount bandwidth used. + * + * Note: This Metric should actually inherit from AggregatedMetric class, but we need to use ProcessedMetric as core + * won't format the values automatically otherwise + */ +class OverallBandwidth extends ProcessedMetric +{ + public function getName() + { + return 'nb_total_overall_bandwidth'; + } + + public function getDependentMetrics() + { + return [ + $this->getName() + ]; + } + + public function compute(Row $row) + { + return $row->getColumn($this->getName()); + } + + public function getTranslatedName() + { + return Piwik::translate('Bandwidth_ColumnTotalOverallBandwidth'); + } + + public function format($value, Formatter $formatter) + { + if ($value) { + $value = $formatter->getPrettySizeFromBytes($value, null, 2); + } + + return $value; + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/PageviewBandwidth.php b/plugins/Bandwidth/Columns/Metrics/PageviewBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..b0b54641b2361b96e93368fd9f5c57196f06f499 --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/PageviewBandwidth.php @@ -0,0 +1,61 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\DataTable\Row; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Columns\Dimension; +use Piwik\Plugin\ProcessedMetric; + +/** + * The total amount bandwidth used for page views. + * + * Note: This Metric should actually inherit from AggregatedMetric class, but we need to use ProcessedMetric as core + * won't format the values automatically otherwise + */ +class PageviewBandwidth extends ProcessedMetric +{ + public function getName() + { + return 'nb_total_pageview_bandwidth'; + } + + public function getDependentMetrics() + { + return [ + $this->getName() + ]; + } + + public function compute(Row $row) + { + return $row->getColumn($this->getName()); + } + + public function getTranslatedName() + { + return Piwik::translate('Bandwidth_ColumnTotalPageviewBandwidth'); + } + + public function format($value, Formatter $formatter) + { + if ($value) { + $value = $formatter->getPrettySizeFromBytes($value, null, 2); + } + + return $value; + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Bandwidth/Columns/Metrics/SumBandwidth.php b/plugins/Bandwidth/Columns/Metrics/SumBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..e82339de762b512cec94f920ca343bb558a15631 --- /dev/null +++ b/plugins/Bandwidth/Columns/Metrics/SumBandwidth.php @@ -0,0 +1,37 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\Columns\Metrics; + +use Piwik\Piwik; +use Piwik\Columns\Dimension; +use Piwik\Plugins\Bandwidth\Metrics; + +/** + * The sum amount bandwidth of a pages. + */ +class SumBandwidth extends Base +{ + protected $metric = Metrics::METRICS_PAGE_SUM_BANDWIDTH; + + public function getName() + { + return 'sum_bandwidth'; + } + + public function getTranslatedName() + { + return Piwik::translate('Bandwidth_ColumnSumBandwidth'); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/Bandwidth/Controller.php b/plugins/Bandwidth/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..2a846701e647b932ae13982e376b927d68b4ab1b --- /dev/null +++ b/plugins/Bandwidth/Controller.php @@ -0,0 +1,30 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth; + +use Piwik\Common; +use Piwik\Piwik; + +/** + * + */ +class Controller extends \Piwik\Plugin\Controller +{ + public function getEvolutionGraph() + { + $columns = Common::getRequestVar('columns'); + $columns = Piwik::getArrayFromApiParameter($columns); + + $view = $this->getLastUnitGraphAcrossPlugins($this->pluginName, __FUNCTION__, $columns, [], ''); + + return $this->renderView($view); + } +} diff --git a/plugins/Bandwidth/LICENSE b/plugins/Bandwidth/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7 --- /dev/null +++ b/plugins/Bandwidth/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/plugins/Bandwidth/Metrics.php b/plugins/Bandwidth/Metrics.php new file mode 100644 index 0000000000000000000000000000000000000000..9e91b698eb966459de65fd72712569a37c12daa4 --- /dev/null +++ b/plugins/Bandwidth/Metrics.php @@ -0,0 +1,134 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth; + +use Piwik\Piwik; +use Piwik\Columns\Dimension; +use Piwik\Plugins\Bandwidth\Columns\Bandwidth as BandwidthColumn; +use Piwik\Plugins\Bandwidth\Columns\Metrics\AvgBandwidth; +use Piwik\Plugins\Bandwidth\Columns\Metrics\DownloadBandwidth; +use Piwik\Plugins\Bandwidth\Columns\Metrics\HitsWithBandwidth; +use Piwik\Plugins\Bandwidth\Columns\Metrics\MaxBandwidth; +use Piwik\Plugins\Bandwidth\Columns\Metrics\MinBandwidth; +use Piwik\Plugins\Bandwidth\Columns\Metrics\OverallBandwidth; +use Piwik\Plugins\Bandwidth\Columns\Metrics\PageviewBandwidth; +use Piwik\Plugins\Bandwidth\Columns\Metrics\SumBandwidth; + +class Metrics +{ + public const METRICS_PAGE_SUM_BANDWIDTH = 1090; + public const METRICS_PAGE_MIN_BANDWIDTH = 1091; + public const METRICS_PAGE_MAX_BANDWIDTH = 1092; + public const METRICS_NB_HITS_WITH_BANDWIDTH = 1093; + + public const COLUMN_TOTAL_OVERALL_BANDWIDTH = 'nb_total_overall_bandwidth'; + public const COLUMN_TOTAL_PAGEVIEW_BANDWIDTH = 'nb_total_pageview_bandwidth'; + public const COLUMN_TOTAL_DOWNLOAD_BANDWIDTH = 'nb_total_download_bandwidth'; + + public static function getNumericRecordNameToColumnsMapping() + { + return [ + Archiver::BANDWIDTH_TOTAL_RECORD => Metrics::COLUMN_TOTAL_OVERALL_BANDWIDTH, + Archiver::BANDWIDTH_PAGEVIEW_RECORD => Metrics::COLUMN_TOTAL_PAGEVIEW_BANDWIDTH, + Archiver::BANDWIDTH_DOWNLOAD_RECORD => Metrics::COLUMN_TOTAL_DOWNLOAD_BANDWIDTH, + ]; + } + + /** + * @return \Piwik\Plugins\Bandwidth\Columns\Metrics\Base[] + */ + public static function getBandwidthMetrics() + { + return [ + new HitsWithBandwidth(), + new MaxBandwidth(), + new MinBandwidth(), + new SumBandwidth(), + new AvgBandwidth(), + ]; + } + + /** + * @return \Piwik\Plugin\Metric[] + */ + public static function getOverallMetrics() + { + return [ + new DownloadBandwidth(), + new PageviewBandwidth(), + new OverallBandwidth(), + ]; + } + + public static function getMetricTranslations() + { + $translations = []; + foreach (self::getBandwidthMetrics() as $metric) { + $translations[$metric->getName()] = $metric->getTranslatedName(); + } + + $translations[Metrics::COLUMN_TOTAL_OVERALL_BANDWIDTH] = Piwik::translate('Bandwidth_ColumnTotalOverallBandwidth'); + $translations[Metrics::COLUMN_TOTAL_PAGEVIEW_BANDWIDTH] = Piwik::translate('Bandwidth_ColumnTotalPageviewBandwidth'); + $translations[Metrics::COLUMN_TOTAL_DOWNLOAD_BANDWIDTH] = Piwik::translate('Bandwidth_ColumnTotalDownloadBandwidth'); + + return $translations; + } + + public static function getMetricSemanticTypes(): array + { + $semanticTypes = []; + foreach (self::getBandwidthMetrics() as $metric) { + $semanticTypes[$metric->getName()] = $metric->getSemanticType(); + } + + $semanticTypes[Metrics::COLUMN_TOTAL_OVERALL_BANDWIDTH] = Dimension::TYPE_NUMBER; + $semanticTypes[Metrics::COLUMN_TOTAL_PAGEVIEW_BANDWIDTH] = Dimension::TYPE_NUMBER; + $semanticTypes[Metrics::COLUMN_TOTAL_DOWNLOAD_BANDWIDTH] = Dimension::TYPE_NUMBER; + + return $semanticTypes; + } + + public static function getActionMetrics() + { + $column = new BandwidthColumn(); + $column = $column->getColumnName(); + + $metricsConfig = []; + $metricsConfig[self::METRICS_PAGE_SUM_BANDWIDTH] = [ + 'aggregation' => 'sum', + 'query' => "sum( + case when $column is null + then 0 + else $column + end + )", + ]; + $metricsConfig[self::METRICS_NB_HITS_WITH_BANDWIDTH] = [ + 'aggregation' => 'sum', + 'query' => "sum( + case when $column is null + then 0 + else 1 + end + )", + ]; + $metricsConfig[self::METRICS_PAGE_MIN_BANDWIDTH] = [ + 'aggregation' => 'min', + 'query' => "min($column)", + ]; + $metricsConfig[self::METRICS_PAGE_MAX_BANDWIDTH] = [ + 'aggregation' => 'max', + 'query' => "max($column)", + ]; + + return $metricsConfig; + } +} diff --git a/plugins/Bandwidth/ProfileSummary/BandwidthSummary.php b/plugins/Bandwidth/ProfileSummary/BandwidthSummary.php new file mode 100644 index 0000000000000000000000000000000000000000..e68457400454c34985fe2612a7016bf561333e0e --- /dev/null +++ b/plugins/Bandwidth/ProfileSummary/BandwidthSummary.php @@ -0,0 +1,51 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth\ProfileSummary; + +use Piwik\Piwik; +use Piwik\Plugins\Live\ProfileSummary\ProfileSummaryAbstract; +use Piwik\View; + +/** + * Class BandwidthSummary + */ +class BandwidthSummary extends ProfileSummaryAbstract +{ + /** + * @inheritdoc + */ + public function getName() + { + return Piwik::translate('Bandwidth_Bandwidth'); + } + + /** + * @inheritdoc + */ + public function render() + { + if (!isset($this->profile['avgBandwidth'])) { + return ''; + } + + $view = new View('@Bandwidth/_profileSummary.twig'); + $view->visitorData = $this->profile; + return $view->render(); + } + + /** + * @inheritdoc + */ + public function getOrder() + { + return 70; + } +} diff --git a/plugins/Bandwidth/README.md b/plugins/Bandwidth/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9b32d630eebe98cf8568251865a8945d36c0d8aa --- /dev/null +++ b/plugins/Bandwidth/README.md @@ -0,0 +1,11 @@ +# Matomo Bandwidth Plugin + +[![Build Status](https://github.com/matomo-org/plugin-Bandwidth/actions/workflows/matomo-tests.yml/badge.svg?branch=4.x-dev)](https://github.com/matomo-org/plugin-Bandwidth/actions/workflows/matomo-tests.yml) + +## Description + +This plugin allows you to measure the bandwidth that was used by each page view or download. +It enriches existing reports and APIs to show the used bandwidth. + +This feature only works when using the plugin in combination with our [Log Analytics tracking method](https://matomo.org/log-analytics/) or if you use our HTTP Tracking API directly. It does not work with our standard JavaScript or image tracker. Find more information in the FAQ. + diff --git a/plugins/Bandwidth/RecordBuilders/Base.php b/plugins/Bandwidth/RecordBuilders/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..bfb83faafa05bc59addf929096c07f4f3f5c80c0 --- /dev/null +++ b/plugins/Bandwidth/RecordBuilders/Base.php @@ -0,0 +1,54 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth\RecordBuilders; + +use Piwik\ArchiveProcessor\RecordBuilder; +use Piwik\DataAccess\LogAggregator; +use Piwik\Plugins\Bandwidth\Columns\Bandwidth as BandwidthColumn; + +abstract class Base extends RecordBuilder +{ + protected function queryActionsByDimension(LogAggregator $logAggregator, $extraWhere = null, $joinLogActionOnColumn = false) + { + $column = new BandwidthColumn(); + $column = $column->getColumnName(); + $table = 'log_link_visit_action'; + $field = 'sum_bandwidth'; + $where = "$table.$column is not null"; + + $selects = [ + "sum($table.$column) as `$field`", + ]; + + if (!empty($extraWhere)) { + $where = "$where AND $extraWhere"; + } + + return $logAggregator->queryActionsByDimension([$column], $where, $selects, false, null, $joinLogActionOnColumn); + } + + /** + * @param \Zend_Db_Statement $query + * @param string $field + */ + protected function sumNumericRecord($query, string $field): int + { + $total = 0; + + while ($row = $query->fetch()) { + if (!empty($row[$field])) { + $total += $row[$field]; + } + } + + return (int)$total; + } +} diff --git a/plugins/Bandwidth/RecordBuilders/TotalDownload.php b/plugins/Bandwidth/RecordBuilders/TotalDownload.php new file mode 100644 index 0000000000000000000000000000000000000000..9eefdaa559cffe1966d8496696de7c425566a581 --- /dev/null +++ b/plugins/Bandwidth/RecordBuilders/TotalDownload.php @@ -0,0 +1,37 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth\RecordBuilders; + +use Piwik\ArchiveProcessor; +use Piwik\Plugins\Bandwidth\Archiver; +use Piwik\Tracker\Action; + +class TotalDownload extends Base +{ + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + return [ + ArchiveProcessor\Record::make(ArchiveProcessor\Record::TYPE_NUMERIC, Archiver::BANDWIDTH_DOWNLOAD_RECORD), + ]; + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + $joinLogActionOnColumn = ['idaction_url']; + $query = $this->queryActionsByDimension( + $archiveProcessor->getLogAggregator(), + 'log_action1.type = ' . Action::TYPE_DOWNLOAD, + $joinLogActionOnColumn + ); + $record = $this->sumNumericRecord($query, 'sum_bandwidth'); + return [Archiver::BANDWIDTH_DOWNLOAD_RECORD => $record]; + } +} diff --git a/plugins/Bandwidth/RecordBuilders/TotalOverall.php b/plugins/Bandwidth/RecordBuilders/TotalOverall.php new file mode 100644 index 0000000000000000000000000000000000000000..06c8deaa00c40607d4b1f8fa107804d68fc4da46 --- /dev/null +++ b/plugins/Bandwidth/RecordBuilders/TotalOverall.php @@ -0,0 +1,31 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth\RecordBuilders; + +use Piwik\ArchiveProcessor; +use Piwik\Plugins\Bandwidth\Archiver; + +class TotalOverall extends Base +{ + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + return [ + ArchiveProcessor\Record::make(ArchiveProcessor\Record::TYPE_NUMERIC, Archiver::BANDWIDTH_TOTAL_RECORD), + ]; + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + $query = $this->queryActionsByDimension($archiveProcessor->getLogAggregator()); + $record = $this->sumNumericRecord($query, 'sum_bandwidth'); + return [Archiver::BANDWIDTH_TOTAL_RECORD => $record]; + } +} diff --git a/plugins/Bandwidth/RecordBuilders/TotalPage.php b/plugins/Bandwidth/RecordBuilders/TotalPage.php new file mode 100644 index 0000000000000000000000000000000000000000..127b389969d5e3fbcad4001f9cc21d9155292820 --- /dev/null +++ b/plugins/Bandwidth/RecordBuilders/TotalPage.php @@ -0,0 +1,37 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth\RecordBuilders; + +use Piwik\ArchiveProcessor; +use Piwik\Plugins\Bandwidth\Archiver; +use Piwik\Tracker\Action; + +class TotalPage extends Base +{ + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + return [ + ArchiveProcessor\Record::make(ArchiveProcessor\Record::TYPE_NUMERIC, Archiver::BANDWIDTH_PAGEVIEW_RECORD), + ]; + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + $joinLogActionOnColumn = ['idaction_url']; + $query = $this->queryActionsByDimension( + $archiveProcessor->getLogAggregator(), + 'log_action1.type = ' . Action::TYPE_PAGE_URL, + $joinLogActionOnColumn + ); + $record = $this->sumNumericRecord($query, 'sum_bandwidth'); + return [Archiver::BANDWIDTH_PAGEVIEW_RECORD => $record]; + } +} diff --git a/plugins/Bandwidth/Reports/Base.php b/plugins/Bandwidth/Reports/Base.php new file mode 100644 index 0000000000000000000000000000000000000000..07ad2494808ffe3dde4912d6bc92c1e554d9d67a --- /dev/null +++ b/plugins/Bandwidth/Reports/Base.php @@ -0,0 +1,21 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth\Reports; + +use Piwik\Plugin\Report; + +abstract class Base extends Report +{ + protected function init() + { + $this->categoryId = 'General_Actions'; + } +} diff --git a/plugins/Bandwidth/Reports/Get.php b/plugins/Bandwidth/Reports/Get.php new file mode 100644 index 0000000000000000000000000000000000000000..9693eb90245778e50f6248420bfa3886c1db8ec2 --- /dev/null +++ b/plugins/Bandwidth/Reports/Get.php @@ -0,0 +1,33 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth\Reports; + +use Piwik\Piwik; +use Piwik\Plugins\Bandwidth\Metrics; + +/** + * This class defines a new report. + * + * See {@link http://developer.piwik.org/api-reference/Piwik/Plugin/Report} for more information. + */ +class Get extends Base +{ + protected function init() + { + parent::init(); + + $this->name = Piwik::translate('Bandwidth_Bandwidth') . ' - ' . Piwik::translate('General_MainMetrics'); + $this->order = 30; + $this->metrics = array_values(Metrics::getNumericRecordNameToColumnsMapping()); + // Currently needed to get the metrics formatted + $this->processedMetrics = Metrics::getOverallMetrics(); + } +} diff --git a/plugins/Bandwidth/VisitorDetails.php b/plugins/Bandwidth/VisitorDetails.php new file mode 100644 index 0000000000000000000000000000000000000000..a937fd604ec16ebb1ff9efaa9ea5ec71c9360291 --- /dev/null +++ b/plugins/Bandwidth/VisitorDetails.php @@ -0,0 +1,75 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Bandwidth; + +use Piwik\Metrics\Formatter; +use Piwik\Plugins\Live\VisitorDetailsAbstract; +use Piwik\View; + +class VisitorDetails extends VisitorDetailsAbstract +{ + public function extendActionDetails(&$action, $nextAction, $visitorDetails) + { + if (array_key_exists('bandwidth', $action)) { + $formatter = new Formatter(); + $action['bandwidth_pretty'] = $formatter->getPrettySizeFromBytes($action['bandwidth']); + } + } + + public function renderActionTooltip($action, $visitInfo) + { + if (empty($action['bandwidth'])) { + return []; + } + + $view = new View('@Bandwidth/_actionTooltip'); + $view->action = $action; + return [[60, $view->render()]]; + } + + protected $sumBandwidth = 0; + protected $actionsWithBandwidth = 0; + protected $maxBandwidth = null; + protected $minBandwidth = null; + + public function initProfile($visits, &$profile) + { + $this->sumBandwidth = 0; + $this->actionsWithBandwidth = 0; + } + + public function handleProfileAction($action, &$profile) + { + if (!empty($action['bandwidth'])) { + $this->sumBandwidth += $action['bandwidth']; + if (is_null($this->minBandwidth)) { + $this->minBandwidth = $action['bandwidth']; + } + $this->minBandwidth = min($this->minBandwidth, $action['bandwidth']); + if (is_null($this->minBandwidth)) { + $this->minBandwidth = $action['bandwidth']; + } + $this->maxBandwidth = max($this->maxBandwidth, $action['bandwidth']); + $this->actionsWithBandwidth++; + } + } + + public function finalizeProfile($visits, &$profile) + { + if ($this->actionsWithBandwidth) { + $formatter = new Formatter(); + $profile['sumBandwidth'] = $formatter->getPrettySizeFromBytes($this->sumBandwidth); + $profile['minBandwidth'] = $formatter->getPrettySizeFromBytes($this->minBandwidth); + $profile['maxBandwidth'] = $formatter->getPrettySizeFromBytes($this->maxBandwidth); + $profile['avgBandwidth'] = $formatter->getPrettySizeFromBytes(round($this->sumBandwidth / $this->actionsWithBandwidth)); + } + } +} diff --git a/plugins/Bandwidth/docs/faq.md b/plugins/Bandwidth/docs/faq.md new file mode 100644 index 0000000000000000000000000000000000000000..3a36d632b9d67cdda586f68bfb565de0a19454ec --- /dev/null +++ b/plugins/Bandwidth/docs/faq.md @@ -0,0 +1,44 @@ +## FAQ + +__How can I track the bandwidth?__ + +Log analytics: + +The bandwidth will be automatically tracked when using the [log importer](http://piwik.org/log-analytics/) as long as +your log format is supported. + +Tracking API: + +If you are using the [HTTP Tracking API](http://developer.piwik.org/api-reference/tracking-api) +you can specify the bandwidth in bytes by appending the URL parameter `bw_bytes=1234` to the tracking URL. In this case +a bandwidth of 1234 bytes will be tracked. + +__Which actions support tracking of bandwidth?__ + +Pageviews (Page URLs and Page Titles) as well as Downloads. + +__In which reports is the used bandwidth displayed?__ + +* Page URLs +* Page Titles +* Downloads + +All reports will show a column `Average Bandwidth` and `Sum Bandwidth` + +The "Visitors => Overview" report shows a total bandwidth overview and it is possible to view the evolution over period. + +__Which APIs does this plugin define or enrich?__ + +There is a report `Bandwidth.get` returning the total bandwidth (across all actions). + +It also enriches varies reports such as `Actions.get`, `Actions.getPageUrls`, `Actions.getPageTitles` and `Actions.getDownloads`. +For example it adds columns such as `avg_bandwidth`, `sum_bandwidth`, `min_bandwidth`, `max_bandwidth` to each page view. + +__Why are the bandwidth columns are not displayed in the UI?__ + +Make sure the Bandwidth plugin is activated by going to `Administration => Plugins`. Also the bandwidth columns are not +displayed if no bandwidth was tracked in the current selected month. + +__Is it possible to track bandwidth using the Matomo JavaScript Tracker?__ + +Yes, you can set the `bw_bytes` value manually in JavaScript using `_paq.push(['appendToTrackingUrl', 'bw_bytes=1234']);` to track the bandwidth of your pages. diff --git a/plugins/Bandwidth/lang/am.json b/plugins/Bandwidth/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/am.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/ar.json b/plugins/Bandwidth/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..53e0544c04faefdacbe007fdc4c6e9656182475e --- /dev/null +++ b/plugins/Bandwidth/lang/ar.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "عرض النطاق", + "ColumnAvgBandwidth": "عرض النطاق في المتوسط", + "ColumnSumBandwidth": "مجموع عرض النطاق", + "ColumnMaxBandwidth": "أقصى عرض نطاق", + "ColumnMinBandwidth": "أقل عرض نطاق", + "ColumnHitsWithBandwidth": "الزيارات المسجلة في عرض النطاق", + "ColumnTotalOverallBandwidth": "إجمالي البايتات المنقولة", + "ColumnTotalPageviewBandwidth": "البايتات المنقولة لمرات عرض الصفحات", + "ColumnTotalDownloadBandwidth": "البايتات المنقولة للتنزيلات", + "SparklineTotalBandwidthDetails": "إجمالي البايتات المنقولة %s" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/az.json b/plugins/Bandwidth/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/az.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/be.json b/plugins/Bandwidth/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/be.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/bg.json b/plugins/Bandwidth/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..b8ac7e3dd7b2b37f87d322b58dd2839cd32908b3 --- /dev/null +++ b/plugins/Bandwidth/lang/bg.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Трафик", + "ColumnAvgBandwidth": "Среден трафик", + "ColumnHitsWithBandwidth": "Записани посещения с трафик", + "ColumnMaxBandwidth": "Максимален трафик", + "ColumnMinBandwidth": "Минимален трафик", + "ColumnSumBandwidth": "Общ трафик", + "ColumnTotalDownloadBandwidth": "Прехвърлени байтове (изтегляния)", + "ColumnTotalOverallBandwidth": "Общо прехвърлени байтове", + "ColumnTotalPageviewBandwidth": "Прехвърлени байтове при показвания на страници", + "SparklineTotalBandwidthDetails": "%s байтове, прехвърлени общо" + } +} diff --git a/plugins/Bandwidth/lang/bn.json b/plugins/Bandwidth/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/bn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/bs.json b/plugins/Bandwidth/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/bs.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/ca.json b/plugins/Bandwidth/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..b5cefcd0ef0ff669c1265ef6138e908ca2319d20 --- /dev/null +++ b/plugins/Bandwidth/lang/ca.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Amplada de banda", + "ColumnAvgBandwidth": "Amplada de banda mitja", + "ColumnHitsWithBandwidth": "Visites registrades amb ample de banda", + "ColumnMaxBandwidth": "Ample de banda màxim", + "ColumnMinBandwidth": "Ample de banda mínim", + "ColumnSumBandwidth": "Sumatori d'ample de banda", + "ColumnTotalDownloadBandwidth": "Bytes transferits en descàrregues", + "ColumnTotalOverallBandwidth": "Total de bytes transferits", + "ColumnTotalPageviewBandwidth": "Bytes transferits en pàgines vistes", + "SparklineTotalBandwidthDetails": "%s total de bytes transferits" + } +} diff --git a/plugins/Bandwidth/lang/cs.json b/plugins/Bandwidth/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..d600f5fb371834707b8b410fc5ec5a24e31cd481 --- /dev/null +++ b/plugins/Bandwidth/lang/cs.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Šířka pásma", + "ColumnAvgBandwidth": "Prům. šířka pásma", + "ColumnSumBandwidth": "Celková šířka pásma", + "ColumnMaxBandwidth": "Maximální šířka pásma", + "ColumnMinBandwidth": "Minimální šířka pásma", + "ColumnHitsWithBandwidth": "Zaznamenané zásahy s šířkou pásma", + "ColumnTotalOverallBandwidth": "Celkem přeneseno bytů", + "ColumnTotalPageviewBandwidth": "Přeneseno bytů zobrazení stránek", + "ColumnTotalDownloadBandwidth": "Přeneseno bytů stahování", + "SparklineTotalBandwidthDetails": "Celkem přeneseno %s bytů" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/cy.json b/plugins/Bandwidth/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/cy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/da.json b/plugins/Bandwidth/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..d302483aba5b834ed1fd9ed4c7cd37a730a1d0b8 --- /dev/null +++ b/plugins/Bandwidth/lang/da.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Båndbredde", + "ColumnAvgBandwidth": "Gennemsnitlig båndbredde", + "ColumnSumBandwidth": "Sum båndbredde", + "ColumnMaxBandwidth": "Maks. båndbredde", + "ColumnMinBandwidth": "Min. båndbredde", + "ColumnHitsWithBandwidth": "Registrerede hits med båndbredde", + "ColumnTotalOverallBandwidth": "Bytes samlet overført", + "ColumnTotalPageviewBandwidth": "Bytes overført ved sidevisninger", + "ColumnTotalDownloadBandwidth": "Bytes overført ved filhentning", + "SparklineTotalBandwidthDetails": "%s bytes overført samlet" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/de.json b/plugins/Bandwidth/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..ef249f5faf1f6a8d3d256daa29ce363dba2e8b29 --- /dev/null +++ b/plugins/Bandwidth/lang/de.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Bandbreite", + "ColumnAvgBandwidth": "Durchschn. Bandbreite", + "ColumnHitsWithBandwidth": "Aufgezeichnete Zugriffe mit Bandbreite", + "ColumnMaxBandwidth": "Maximale Bandbreite", + "ColumnMinBandwidth": "Minimale Bandbreite", + "ColumnSumBandwidth": "Bandbreitensumme", + "ColumnTotalDownloadBandwidth": "Durch Downloads übertragene Bytes", + "ColumnTotalOverallBandwidth": "Insgesamt übertragene Bytes", + "ColumnTotalPageviewBandwidth": "Durch Seitenzugriffe übertragene Bytes", + "SparklineTotalBandwidthDetails": "%s Bytes insgesamt übertragen" + } +} diff --git a/plugins/Bandwidth/lang/dv.json b/plugins/Bandwidth/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/el.json b/plugins/Bandwidth/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..db042c6e3d28b1524d5a8f62a4082ee0f83c8830 --- /dev/null +++ b/plugins/Bandwidth/lang/el.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Εύρος ζώνης", + "ColumnAvgBandwidth": "Μέση τιμή εύρους ζώνης", + "ColumnHitsWithBandwidth": "Καταγεγραμμένες επισκέψεις με εύρος ζώνης", + "ColumnMaxBandwidth": "Μέγιστη τιμή εύρους ζώνης", + "ColumnMinBandwidth": "Ελάχιστη τιμή εύρους ζώνης", + "ColumnSumBandwidth": "Άθροισμα τιμών εύρους ζώνης", + "ColumnTotalDownloadBandwidth": "Bytes που μεταφέρθηκαν για κατέβασμα", + "ColumnTotalOverallBandwidth": "Συνολικά bytes που μεταφέρθηκαν", + "ColumnTotalPageviewBandwidth": "Bytes από την μεταφορά αναγνώσεων σελίδων", + "SparklineTotalBandwidthDetails": "%s συνολικά bytes που μεταφέρθηκαν" + } +} diff --git a/plugins/Bandwidth/lang/en.json b/plugins/Bandwidth/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..a6a53cdae81039c25687a60de9738f70db7a75b0 --- /dev/null +++ b/plugins/Bandwidth/lang/en.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Bandwidth", + "ColumnAvgBandwidth": "Avg. Bandwidth", + "ColumnSumBandwidth": "Sum Bandwidth", + "ColumnMaxBandwidth": "Max Bandwidth", + "ColumnMinBandwidth": "Min Bandwidth", + "ColumnHitsWithBandwidth": "Recorded hits with bandwidth", + "ColumnTotalOverallBandwidth": "Bytes transferred overall", + "ColumnTotalPageviewBandwidth": "Bytes transferred pageviews", + "ColumnTotalDownloadBandwidth": "Bytes transferred downloads", + "SparklineTotalBandwidthDetails": "%s bytes transferred overall" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/eo.json b/plugins/Bandwidth/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/eo.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/es-ar.json b/plugins/Bandwidth/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..7542381ef9ad03a9e295a8f23c64b48fcfc70d5d --- /dev/null +++ b/plugins/Bandwidth/lang/es-ar.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Ancho de banda", + "ColumnAvgBandwidth": "Ancho de banda promedio", + "ColumnSumBandwidth": "Ancho de banda total", + "ColumnMaxBandwidth": "Ancho de banda máximo", + "ColumnMinBandwidth": "Ancho de banda mínimo", + "ColumnHitsWithBandwidth": "Eventos registrados con ancho de banda", + "ColumnTotalOverallBandwidth": "Total de bytes transferidos", + "ColumnTotalPageviewBandwidth": "Bytes transferidos en vistas de páginas", + "ColumnTotalDownloadBandwidth": "Bytes transferidos en descargas", + "SparklineTotalBandwidthDetails": "%s bytes transferidos en total" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/es.json b/plugins/Bandwidth/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..166461ea326ff83bf64ad27f8950a0b48da6198f --- /dev/null +++ b/plugins/Bandwidth/lang/es.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Ancho de banda", + "ColumnAvgBandwidth": "Ancho de banda promedio", + "ColumnHitsWithBandwidth": "Accesos registrados con ancho de banda", + "ColumnMaxBandwidth": "Máximo ancho de banda", + "ColumnMinBandwidth": "Ancho de bando mínimo", + "ColumnSumBandwidth": "Sumatoria de ancho de banda", + "ColumnTotalDownloadBandwidth": "Bytes transferidos por descargas", + "ColumnTotalOverallBandwidth": "Bytes generales transferidos", + "ColumnTotalPageviewBandwidth": "Bytes transferidos por páginas vistas", + "SparklineTotalBandwidthDetails": "%s bytes transferidos en general" + } +} diff --git a/plugins/Bandwidth/lang/et.json b/plugins/Bandwidth/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/et.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/eu.json b/plugins/Bandwidth/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..094e3fb7b2632bdecbd73e2b57257aaf217d45b1 --- /dev/null +++ b/plugins/Bandwidth/lang/eu.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Banda-zabalera", + "ColumnAvgBandwidth": "Batez besteko banda-zabalera", + "ColumnHitsWithBandwidth": "Erregistratutako bisitako ekintzak banda zabalerarekin", + "ColumnMaxBandwidth": "Gehienezko banda-zabalera", + "ColumnMinBandwidth": "Gutxieneko banda-zabalera", + "ColumnSumBandwidth": "Banda-zabaleraren batura", + "ColumnTotalDownloadBandwidth": "Deskargetan transferitutako byteak", + "ColumnTotalOverallBandwidth": "Transferitutako byteak guztira", + "ColumnTotalPageviewBandwidth": "Orri-ikustaldietan transferitutako byteak", + "SparklineTotalBandwidthDetails": "%s byte transferituta guztira" + } +} diff --git a/plugins/Bandwidth/lang/fa.json b/plugins/Bandwidth/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/fa.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/fi.json b/plugins/Bandwidth/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..8d5bd78f114d9ab8867f72462331d04ed25ff963 --- /dev/null +++ b/plugins/Bandwidth/lang/fi.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Kaista", + "ColumnAvgBandwidth": "Keskimääräinen kaistankäyttö", + "ColumnSumBandwidth": "Kaistankäyttö yhteensä", + "ColumnMaxBandwidth": "Kaistankäyttö enintään", + "ColumnMinBandwidth": "Kaistankäyttö vähintään", + "ColumnHitsWithBandwidth": "Kaistankäytön kanssa tallennettuja osumia", + "ColumnTotalOverallBandwidth": "tavua siirretty yhteensä", + "ColumnTotalPageviewBandwidth": "tavua siirretty sivunkatseluihin", + "ColumnTotalDownloadBandwidth": "tavua siirretty latauksissa", + "SparklineTotalBandwidthDetails": "%s tavua siirretty yhteensä" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/fr.json b/plugins/Bandwidth/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..6380d7881f4053c453fc8462ac9ae466f115e229 --- /dev/null +++ b/plugins/Bandwidth/lang/fr.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Bande passante", + "ColumnAvgBandwidth": "Bande Passante Moy", + "ColumnHitsWithBandwidth": "Visites enregistrées avec bande passante", + "ColumnMaxBandwidth": "Bande Passante Max", + "ColumnMinBandwidth": "Bande Passante Min", + "ColumnSumBandwidth": "Somme Bande Passante", + "ColumnTotalDownloadBandwidth": "Octets transférés téléchargés", + "ColumnTotalOverallBandwidth": "Octets transférés au total", + "ColumnTotalPageviewBandwidth": "Octets transférés de pages vues", + "SparklineTotalBandwidthDetails": "%s octets transférés au total" + } +} diff --git a/plugins/Bandwidth/lang/ga.json b/plugins/Bandwidth/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..cd36c93d7876f9d02a28e483999dc710ab0e34fb --- /dev/null +++ b/plugins/Bandwidth/lang/ga.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "bandaleithead", + "ColumnAvgBandwidth": "Meánlíon bandaleithead", + "ColumnHitsWithBandwidth": "Amas taifeadta le bandaleithead", + "ColumnMaxBandwidth": "Bandaleithid uasta", + "ColumnMinBandwidth": "Bandaleithid íosta", + "ColumnSumBandwidth": "Bandaleithid Suime", + "ColumnTotalDownloadBandwidth": "Íoslódálacha aistrithe bytes", + "ColumnTotalOverallBandwidth": "Bearta aistrithe ar an iomlán", + "ColumnTotalPageviewBandwidth": "Aistríodh bearta radhairc leathanaigh", + "SparklineTotalBandwidthDetails": "%s beart aistrithe san iomlán" + } +} diff --git a/plugins/Bandwidth/lang/gl.json b/plugins/Bandwidth/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..7390b76d1ea303a3c7c1bbbf457f13336ed8edfc --- /dev/null +++ b/plugins/Bandwidth/lang/gl.json @@ -0,0 +1,7 @@ +{ + "Bandwidth": { + "Bandwidth": "Largo de banda", + "ColumnMaxBandwidth": "Largo de banda máximo", + "ColumnMinBandwidth": "Largo de banda mínimo" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/gu.json b/plugins/Bandwidth/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..f8462b4a71ebc5e9e53cd2630b0d01b847ef3ccf --- /dev/null +++ b/plugins/Bandwidth/lang/gu.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "બેન્ડવિડ્થ", + "ColumnAvgBandwidth": "સરેરાશ બેન્ડવિડ્થ", + "ColumnHitsWithBandwidth": "બેન્ડવિડ્થ સાથે રેકોર્ડ કરેલ હિટ", + "ColumnMaxBandwidth": "મહત્તમ બેન્ડવિડ્થ", + "ColumnMinBandwidth": "ન્યૂનતમ બેન્ડવિડ્થ", + "ColumnSumBandwidth": "બેન્ડવિડ્થનો સરવાળો", + "ColumnTotalDownloadBandwidth": "બાઇટ્સ ટ્રાન્સફર ડાઉનલોડ્સ", + "ColumnTotalOverallBandwidth": "એકંદરે સ્થાનાંતરિત બાઇટ્સ", + "ColumnTotalPageviewBandwidth": "બાઇટ્સ ટ્રાન્સફર પૃષ્ઠ દૃશ્યો", + "SparklineTotalBandwidthDetails": "%s બાઇટ્સ એકંદરે ટ્રાન્સફર થયા" + } +} diff --git a/plugins/Bandwidth/lang/he.json b/plugins/Bandwidth/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..1464055d246e70f600f3a20c49f0c2844dceec59 --- /dev/null +++ b/plugins/Bandwidth/lang/he.json @@ -0,0 +1,5 @@ +{ + "Bandwidth": { + "Bandwidth": "רוחב פס" + } +} diff --git a/plugins/Bandwidth/lang/hi.json b/plugins/Bandwidth/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..07d74923d18991c6253e322e49c46508d1bd7711 --- /dev/null +++ b/plugins/Bandwidth/lang/hi.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "बैंडविड्थ", + "ColumnAvgBandwidth": "औसत। बैंडविड्थ", + "ColumnHitsWithBandwidth": "बैंडविड्थ के साथ रिकार्ड किए गए हिट", + "ColumnMaxBandwidth": "अधिकतम बैंडविड्थ", + "ColumnMinBandwidth": "न्यूनतम बैंडविड्थ", + "ColumnSumBandwidth": "योग बैंडविड्थ", + "ColumnTotalDownloadBandwidth": "बाइट्स डाउनलोड का तबादला", + "ColumnTotalOverallBandwidth": "बाइट्स समग्र का तबादला", + "ColumnTotalPageviewBandwidth": "हस्तांतरित प्रिव्यू बाइट्स", + "SparklineTotalBandwidthDetails": "%s बाइट्स समग्र रूप से स्थानांतरित हो गए" + } +} diff --git a/plugins/Bandwidth/lang/hr.json b/plugins/Bandwidth/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..7f8e7271c02e11e5ce4581377cc466ea1b8abc13 --- /dev/null +++ b/plugins/Bandwidth/lang/hr.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Propusnost", + "ColumnAvgBandwidth": "Prosječna propusnost", + "ColumnHitsWithBandwidth": "Zabilježeni pogoci s propusnosti", + "ColumnMaxBandwidth": "Maksimalna propusnost", + "ColumnMinBandwidth": "Minimalna propusnost", + "ColumnSumBandwidth": "Ukupna propusnost", + "ColumnTotalDownloadBandwidth": "Broj prenesenih bajtova za preuzimanja", + "ColumnTotalOverallBandwidth": "Ukupni broj prenesenih bajtova", + "ColumnTotalPageviewBandwidth": "Broj prenesenih bajtova za prikaze stranice", + "SparklineTotalBandwidthDetails": "%s ukupno prenesenih bajtova" + } +} diff --git a/plugins/Bandwidth/lang/hu.json b/plugins/Bandwidth/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..c27b592a3d336b552bd5163a343712278334f6b3 --- /dev/null +++ b/plugins/Bandwidth/lang/hu.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Sávszélesség", + "ColumnAvgBandwidth": "Ált. sávszélesség", + "ColumnSumBandwidth": "Össz sávszélesség", + "ColumnMaxBandwidth": "Max sávszélesség", + "ColumnMinBandwidth": "Min sávszélesség", + "ColumnHitsWithBandwidth": "Rögzített találatok sávszélességgel", + "ColumnTotalOverallBandwidth": "Forgalmazott bájtok", + "ColumnTotalPageviewBandwidth": "Oldalbetöltéskor forgalmazott bájtok", + "ColumnTotalDownloadBandwidth": "Letöltéskor forgalmazott bájtok", + "SparklineTotalBandwidthDetails": "%s bájt összforgalom" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/hy.json b/plugins/Bandwidth/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/hy.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/id.json b/plugins/Bandwidth/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..31ef7de9755f948d363638f78c0045eaa3212ea1 --- /dev/null +++ b/plugins/Bandwidth/lang/id.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Lebar pita", + "ColumnAvgBandwidth": "Rata-rata Lebar pita", + "ColumnHitsWithBandwidth": "Hit terekam dengan Lebar pita", + "ColumnMaxBandwidth": "Lebar pita Maksimal", + "ColumnMinBandwidth": "Lebar pita minimal", + "ColumnSumBandwidth": "Jumlah Lebar pita", + "ColumnTotalDownloadBandwidth": "Unduhan Bita terkirim", + "ColumnTotalOverallBandwidth": "Total Bita terkirim", + "ColumnTotalPageviewBandwidth": "Bita halaman dikunjungi terkirim", + "SparklineTotalBandwidthDetails": "%s byte ditransfer secara keseluruhan" + } +} diff --git a/plugins/Bandwidth/lang/is.json b/plugins/Bandwidth/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/is.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/it.json b/plugins/Bandwidth/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..56279544b69b251172aa3c5f59ce860588e7b4c1 --- /dev/null +++ b/plugins/Bandwidth/lang/it.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Ampiezza di banda", + "ColumnAvgBandwidth": "Ampiezza di banda Media", + "ColumnSumBandwidth": "Somma Ampiezza di banda", + "ColumnMaxBandwidth": "Ampiezza di banda Max", + "ColumnMinBandwidth": "Ampiezza di banda Min", + "ColumnHitsWithBandwidth": "Visite registrate con ampiezza di banda", + "ColumnTotalOverallBandwidth": "Bytes complessivi trasferiti", + "ColumnTotalPageviewBandwidth": "Bytes trasferiti nelle viste pagina", + "ColumnTotalDownloadBandwidth": "Bytes trasferito nei downloads", + "SparklineTotalBandwidthDetails": "Trasferiti %s bytes complessivi" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/ja.json b/plugins/Bandwidth/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..0de54988ac851b7764cfa08025e40bf7e801c694 --- /dev/null +++ b/plugins/Bandwidth/lang/ja.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "帯域", + "ColumnAvgBandwidth": "帯域の平均", + "ColumnSumBandwidth": "帯域の合計", + "ColumnMaxBandwidth": "帯域の最大", + "ColumnMinBandwidth": "帯域の最小", + "ColumnHitsWithBandwidth": "帯域に記録されたヒット数", + "ColumnTotalOverallBandwidth": "転送された全バイト数", + "ColumnTotalPageviewBandwidth": "ページビュー転送バイト数", + "ColumnTotalDownloadBandwidth": "ダウンロード転送バイト数", + "SparklineTotalBandwidthDetails": "%s バイト転送(全体)" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/ka.json b/plugins/Bandwidth/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/ka.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/ko.json b/plugins/Bandwidth/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..cb69e4f81e7342ace1d3e531396fefba45d0d878 --- /dev/null +++ b/plugins/Bandwidth/lang/ko.json @@ -0,0 +1,12 @@ +{ + "Bandwidth": { + "Bandwidth": "대역폭", + "ColumnAvgBandwidth": "평균 대역폭", + "ColumnHitsWithBandwidth": "대역폭으로 기록된 조회수", + "ColumnMaxBandwidth": "최대 대역폭", + "ColumnMinBandwidth": "최소 대역폭", + "ColumnSumBandwidth": "합계 대역폭", + "ColumnTotalOverallBandwidth": "전체 전송된 바이트", + "ColumnTotalPageviewBandwidth": "바이트 전송된 페이지뷰" + } +} diff --git a/plugins/Bandwidth/lang/ku.json b/plugins/Bandwidth/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/ku.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/lb.json b/plugins/Bandwidth/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/lb.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/lt.json b/plugins/Bandwidth/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/lt.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/lv.json b/plugins/Bandwidth/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..ecbeb4050e84ba75c046d0ab4e6c1e0757650e93 --- /dev/null +++ b/plugins/Bandwidth/lang/lv.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Josla", + "ColumnAvgBandwidth": "Vid. joslas platums", + "ColumnHitsWithBandwidth": "Reģistrētie apmeklējumi ar joslas platumu", + "ColumnMaxBandwidth": "Maksimālais joslas platums", + "ColumnMinBandwidth": "Minimālais joslas platums", + "ColumnSumBandwidth": "Summēt joslas platumu", + "ColumnTotalDownloadBandwidth": "Pārsūtīto baitu lejupielādes", + "ColumnTotalOverallBandwidth": "Kopējais pārsūtīto baitu skaits", + "ColumnTotalPageviewBandwidth": "Pārsūtītie baiti lapas skatījumi", + "SparklineTotalBandwidthDetails": "Kopumā pārsūtīti %s baiti" + } +} diff --git a/plugins/Bandwidth/lang/ms.json b/plugins/Bandwidth/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..0b819b2ff27cf6ce32c5963d8f4db8ae97c1fb78 --- /dev/null +++ b/plugins/Bandwidth/lang/ms.json @@ -0,0 +1,10 @@ +{ + "Bandwidth": { + "Bandwidth": "Lebar jalur", + "ColumnAvgBandwidth": "Purata Lebar jalur", + "ColumnHitsWithBandwidth": "Hit direkodkan dengan jalur lebar", + "ColumnMaxBandwidth": "Jalur Lebar Maksimum", + "ColumnMinBandwidth": "Jalur Lebar Minimum", + "ColumnSumBandwidth": "Jumlah Jalur Lebar" + } +} diff --git a/plugins/Bandwidth/lang/nb.json b/plugins/Bandwidth/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..dd74f303805ed52686f56a2f037645f662dcf36b --- /dev/null +++ b/plugins/Bandwidth/lang/nb.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Båndbredde", + "ColumnAvgBandwidth": "Gj.snitt. båndbredde", + "ColumnSumBandwidth": "Sum båndbredde", + "ColumnMaxBandwidth": "Maksimum båndbredde", + "ColumnMinBandwidth": "Minimum båndbredde", + "ColumnHitsWithBandwidth": "Innsamlede treff med båndbredde", + "ColumnTotalOverallBandwidth": "Bytes overført totalt", + "ColumnTotalPageviewBandwidth": "Overførte bytes for sidevisninger", + "ColumnTotalDownloadBandwidth": "Overførte bytes for nedlastinger", + "SparklineTotalBandwidthDetails": "%s bytes overført totalt" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/nl.json b/plugins/Bandwidth/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..db7e5eb27cd125c21326161b0a87a80303a549e3 --- /dev/null +++ b/plugins/Bandwidth/lang/nl.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Bandbreedte", + "ColumnAvgBandwidth": "Gem. Bandbreedte", + "ColumnSumBandwidth": "Totale Bandbreedte", + "ColumnMaxBandwidth": "Max. Bandbreedte", + "ColumnMinBandwidth": "Min. Bandbreedte", + "ColumnHitsWithBandwidth": "Gemeten hits met bandbreedte", + "ColumnTotalOverallBandwidth": "Totaal aantal bytes verzonden", + "ColumnTotalPageviewBandwidth": "Bytes verzonden paginaweergaven", + "ColumnTotalDownloadBandwidth": "Bytes verzonden downloads", + "SparklineTotalBandwidthDetails": "%s bytes verzonden totaal" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/nn.json b/plugins/Bandwidth/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/nn.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/pl.json b/plugins/Bandwidth/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..9823308db18d9ced11d7a7df88452727a66fa5fb --- /dev/null +++ b/plugins/Bandwidth/lang/pl.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Transfer", + "ColumnAvgBandwidth": "Średni Transfer", + "ColumnSumBandwidth": "Całkowity Transfer", + "ColumnMaxBandwidth": "Maks. Transfer", + "ColumnMinBandwidth": "Min. Transfer", + "ColumnHitsWithBandwidth": "Odnotowane trafienia z transferu", + "ColumnTotalOverallBandwidth": "Przesłanych łącznie bajtów", + "ColumnTotalPageviewBandwidth": "Bajtów przesłanych podczas wyświetlania stron", + "ColumnTotalDownloadBandwidth": "Bajtów przesłanych podczas pobierania plików", + "SparklineTotalBandwidthDetails": "%s przesłanych łącznie bajtów" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/pt-br.json b/plugins/Bandwidth/lang/pt-br.json new file mode 100644 index 0000000000000000000000000000000000000000..b3c69af3583186efc00f1e358678d57267510fdb --- /dev/null +++ b/plugins/Bandwidth/lang/pt-br.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Largura de banda", + "ColumnAvgBandwidth": "Média da largura de banda", + "ColumnSumBandwidth": "Somatório da largura de banda", + "ColumnMaxBandwidth": "Largura de banda máxima", + "ColumnMinBandwidth": "Largura de banda mínima", + "ColumnHitsWithBandwidth": "Acessos gravados com largura de banda", + "ColumnTotalOverallBandwidth": "Visão geral dos bytes transferidos", + "ColumnTotalPageviewBandwidth": "Bytes transferidos por visualizações de página", + "ColumnTotalDownloadBandwidth": "Bytes transferidos por download", + "SparklineTotalBandwidthDetails": "%s bytes transferidos em geral" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/pt.json b/plugins/Bandwidth/lang/pt.json new file mode 100644 index 0000000000000000000000000000000000000000..585af25b774721556e8e33f99b4c5daadf3d628b --- /dev/null +++ b/plugins/Bandwidth/lang/pt.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Largura de banda", + "ColumnAvgBandwidth": "Largura de banda média", + "ColumnHitsWithBandwidth": "Registos registados com a largura de banda", + "ColumnMaxBandwidth": "Largura de banda máxima", + "ColumnMinBandwidth": "Largura de banda mínima", + "ColumnSumBandwidth": "Somatório de largura de banda", + "ColumnTotalDownloadBandwidth": "Bytes transferidas nas descargas", + "ColumnTotalOverallBandwidth": "bytes transferidos no total", + "ColumnTotalPageviewBandwidth": "Bytes transferidos nas visualizações de páginas", + "SparklineTotalBandwidthDetails": "%s bytes transferidos no total" + } +} diff --git a/plugins/Bandwidth/lang/ro.json b/plugins/Bandwidth/lang/ro.json new file mode 100644 index 0000000000000000000000000000000000000000..7bf2cf631e1bdeb92a8e87a1b4ed8236b205bf72 --- /dev/null +++ b/plugins/Bandwidth/lang/ro.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Lăţime de bandă", + "ColumnAvgBandwidth": "Lăţime de bandă medie", + "ColumnSumBandwidth": "Total lăţime de bandă", + "ColumnMaxBandwidth": "Lăţime de bandă maximă", + "ColumnMinBandwidth": "Lăţime de bandă minimă", + "ColumnHitsWithBandwidth": "Hituri înregistrate pe lăţime de bandă", + "ColumnTotalOverallBandwidth": "Total octeţi tranferaţi", + "ColumnTotalPageviewBandwidth": "Vizualizări octeţi tranferaţi", + "ColumnTotalDownloadBandwidth": "Descărcări octeţi tranferaţi", + "SparklineTotalBandwidthDetails": "%s octeţi tranferaţi total" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/ru.json b/plugins/Bandwidth/lang/ru.json new file mode 100644 index 0000000000000000000000000000000000000000..b5f4d10a770200ad2dd26ac9fbb2c8142534d4f1 --- /dev/null +++ b/plugins/Bandwidth/lang/ru.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Ширина канала", + "ColumnAvgBandwidth": "Средняя ширина канала", + "ColumnSumBandwidth": "Суммарная ширина канала", + "ColumnMaxBandwidth": "Максимальная ширина канала", + "ColumnMinBandwidth": "Минимальная ширина канала", + "ColumnHitsWithBandwidth": "Сохранено хитов с шириной канала", + "ColumnTotalOverallBandwidth": "Байт передано всего", + "ColumnTotalPageviewBandwidth": "Байт передано на просмотр страниц", + "ColumnTotalDownloadBandwidth": "Байт передано на скачивания", + "SparklineTotalBandwidthDetails": "%s байт передано всего" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/si.json b/plugins/Bandwidth/lang/si.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/si.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/sk.json b/plugins/Bandwidth/lang/sk.json new file mode 100644 index 0000000000000000000000000000000000000000..f3a8a971fdec2c98926c7df2ed5afdefb392dbcc --- /dev/null +++ b/plugins/Bandwidth/lang/sk.json @@ -0,0 +1,5 @@ +{ + "Bandwidth": { + "Bandwidth": "Šírka pásma" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/sl.json b/plugins/Bandwidth/lang/sl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/sl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/sq.json b/plugins/Bandwidth/lang/sq.json new file mode 100644 index 0000000000000000000000000000000000000000..9a557764f1ce5a65346a5bc659911b67fa88a38c --- /dev/null +++ b/plugins/Bandwidth/lang/sq.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Gjerësi bande", + "ColumnAvgBandwidth": "Gjerësi Mesatare Bande", + "ColumnHitsWithBandwidth": "Vizita të regjistruara, me gjerësi bande", + "ColumnMaxBandwidth": "Gjerësi Maksimum Bande", + "ColumnMinBandwidth": "Gjerësi Minimum Bande", + "ColumnSumBandwidth": "Shumë Gjerësish Bande", + "ColumnTotalDownloadBandwidth": "Bajte të shpërngulur nga shkarkime", + "ColumnTotalOverallBandwidth": "Bajte të shpërngulur gjithsej", + "ColumnTotalPageviewBandwidth": "Bajte të shpërngulur nga parje faqesh", + "SparklineTotalBandwidthDetails": "%s bajte të shpërngulur gjithsej" + } +} diff --git a/plugins/Bandwidth/lang/sr.json b/plugins/Bandwidth/lang/sr.json new file mode 100644 index 0000000000000000000000000000000000000000..8dd194863c66dae1bd88f1e0dd34c402dfc306d7 --- /dev/null +++ b/plugins/Bandwidth/lang/sr.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Protok", + "ColumnAvgBandwidth": "Prosečan protok", + "ColumnSumBandwidth": "Ukupan protok", + "ColumnMaxBandwidth": "Maks. protok", + "ColumnMinBandwidth": "Min. protok", + "ColumnHitsWithBandwidth": "Zabeleženo pogodaka uz protok", + "ColumnTotalOverallBandwidth": "Ukupno preneseno bajtova", + "ColumnTotalPageviewBandwidth": "Preneseno bajtova na preglede stranica", + "ColumnTotalDownloadBandwidth": "Preneseno bajtova na preuzimanje fajlova", + "SparklineTotalBandwidthDetails": "Ukupno preneseno bajtova %s" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/sv.json b/plugins/Bandwidth/lang/sv.json new file mode 100644 index 0000000000000000000000000000000000000000..bf0549851c0c56a94046a2761f0534d59fde6e18 --- /dev/null +++ b/plugins/Bandwidth/lang/sv.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Bandbredd", + "ColumnAvgBandwidth": "Genomsnittlig bandbredd", + "ColumnSumBandwidth": "Summa bandbredd", + "ColumnMaxBandwidth": "Max bandbredd", + "ColumnMinBandwidth": "Minsta bandbredd", + "ColumnHitsWithBandwidth": "Insamlade träffar med bandbredd", + "ColumnTotalOverallBandwidth": "Överförda bytes totalt", + "ColumnTotalPageviewBandwidth": "Överförda bytes sidvisningar", + "ColumnTotalDownloadBandwidth": "Överförda bytes nedladdningar", + "SparklineTotalBandwidthDetails": "%s byte överförda totalt" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/ta.json b/plugins/Bandwidth/lang/ta.json new file mode 100644 index 0000000000000000000000000000000000000000..5e8c0bf67743c1c232b1a0f694535a4c5ec12a7a --- /dev/null +++ b/plugins/Bandwidth/lang/ta.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "அலைக்கற்றை", + "ColumnAvgBandwidth": "சராசரி அலைக்கற்றை", + "ColumnHitsWithBandwidth": "அலைவரிசையுடன் பதிவு செய்யப்பட்ட வெற்றிகள்", + "ColumnMaxBandwidth": "அலைக்கற்றை அதிக அளவு", + "ColumnMinBandwidth": "அலைக்கற்றை குறைந்த அளவு", + "ColumnSumBandwidth": "அலைக்கற்றை மொத்தம்", + "ColumnTotalDownloadBandwidth": "பைட்ச் மாற்றப்பட்ட பதிவிறக்கங்களை", + "ColumnTotalOverallBandwidth": "பைட்டுகள் ஒட்டுமொத்தமாக மாற்றப்பட்டன", + "ColumnTotalPageviewBandwidth": "பைட்டுகள் மாற்றப்பட்ட பக்கக் காட்சிகள்", + "SparklineTotalBandwidthDetails": "%s பைட்டுகள் ஒட்டுமொத்தமாக மாற்றப்படுகின்றன" + } +} diff --git a/plugins/Bandwidth/lang/te.json b/plugins/Bandwidth/lang/te.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/te.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/th.json b/plugins/Bandwidth/lang/th.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/th.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/tl.json b/plugins/Bandwidth/lang/tl.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/tl.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/tr.json b/plugins/Bandwidth/lang/tr.json new file mode 100644 index 0000000000000000000000000000000000000000..9dfe4ab6e7e0cf0b4f15430ab2e5e3124bc52ad2 --- /dev/null +++ b/plugins/Bandwidth/lang/tr.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Bant genişliği", + "ColumnAvgBandwidth": "Ortalama bant genişliği", + "ColumnHitsWithBandwidth": "Bant genişliği ile kaydedilmiş erişilme", + "ColumnMaxBandwidth": "En fazla bant genişliği", + "ColumnMinBandwidth": "En az bant genişliği", + "ColumnSumBandwidth": "Toplam bant genişliği", + "ColumnTotalDownloadBandwidth": "İndirmeler için aktarılan bayt", + "ColumnTotalOverallBandwidth": "Toplam aktarılan bayt", + "ColumnTotalPageviewBandwidth": "Sayfa gösterimleri için aktarılan bayt", + "SparklineTotalBandwidthDetails": "Toplam %s bayt aktarıldı" + } +} diff --git a/plugins/Bandwidth/lang/tzm.json b/plugins/Bandwidth/lang/tzm.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/tzm.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/uk.json b/plugins/Bandwidth/lang/uk.json new file mode 100644 index 0000000000000000000000000000000000000000..d89c6f6848d457ab042a6889a9675e2545c626ed --- /dev/null +++ b/plugins/Bandwidth/lang/uk.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "Ширина каналу", + "ColumnAvgBandwidth": "Середня ширина каналу", + "ColumnSumBandwidth": "Загальна ширина каналу", + "ColumnMaxBandwidth": "Максимальна ширина каналу", + "ColumnMinBandwidth": "Мінімальна ширина каналу", + "ColumnHitsWithBandwidth": "Збережено хітів з шириною каналу", + "ColumnTotalOverallBandwidth": "Байт передано всього", + "ColumnTotalPageviewBandwidth": "Байт передано на переглядання сторінок", + "ColumnTotalDownloadBandwidth": "Байт передано на завантаження", + "SparklineTotalBandwidthDetails": "%s байт передано всього" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/ur.json b/plugins/Bandwidth/lang/ur.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/Bandwidth/lang/ur.json @@ -0,0 +1 @@ +{} diff --git a/plugins/Bandwidth/lang/vi.json b/plugins/Bandwidth/lang/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..e857a207f477ac723381dfeb6acd0fe01e136e8f --- /dev/null +++ b/plugins/Bandwidth/lang/vi.json @@ -0,0 +1,9 @@ +{ + "Bandwidth": { + "Bandwidth": "Băng thông", + "ColumnAvgBandwidth": "Băng thông trung bình", + "ColumnSumBandwidth": "Tổng băng thông", + "ColumnMaxBandwidth": "Băng thông tối đa", + "ColumnMinBandwidth": "Băng thông tối thiểu" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/zh-cn.json b/plugins/Bandwidth/lang/zh-cn.json new file mode 100644 index 0000000000000000000000000000000000000000..e4be88474f8b8415d9a0490b5e5c8879864cc8b3 --- /dev/null +++ b/plugins/Bandwidth/lang/zh-cn.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "带宽", + "ColumnAvgBandwidth": "平均带宽", + "ColumnSumBandwidth": "总流量", + "ColumnMaxBandwidth": "最大带宽", + "ColumnMinBandwidth": "最小带宽", + "ColumnHitsWithBandwidth": "记录点击流量", + "ColumnTotalOverallBandwidth": "总传输字节数", + "ColumnTotalPageviewBandwidth": "传输的字节数", + "ColumnTotalDownloadBandwidth": "下载的字节数", + "SparklineTotalBandwidthDetails": "总共传输%s字节" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/lang/zh-tw.json b/plugins/Bandwidth/lang/zh-tw.json new file mode 100644 index 0000000000000000000000000000000000000000..527c2b87f1995ffdc351bbe294765290dae23888 --- /dev/null +++ b/plugins/Bandwidth/lang/zh-tw.json @@ -0,0 +1,14 @@ +{ + "Bandwidth": { + "Bandwidth": "流量", + "ColumnAvgBandwidth": "平均流量", + "ColumnSumBandwidth": "流量總計", + "ColumnMaxBandwidth": "最大流量", + "ColumnMinBandwidth": "最小流量", + "ColumnHitsWithBandwidth": "命中流量的紀錄", + "ColumnTotalOverallBandwidth": "Bytes 傳輸總計", + "ColumnTotalPageviewBandwidth": "Bytes 網頁瀏覽傳輸", + "ColumnTotalDownloadBandwidth": "Bytes 下載傳輸", + "SparklineTotalBandwidthDetails": "%s bytes 傳輸總計" + } +} \ No newline at end of file diff --git a/plugins/Bandwidth/phpcs.xml b/plugins/Bandwidth/phpcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..e2d71189d9c0095191cc824e07e308a1a1c261fe --- /dev/null +++ b/plugins/Bandwidth/phpcs.xml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<ruleset name="bandwidth" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"> + + <description>Matomo Coding Standard for Bandwidth plugin</description> + + <arg name="extensions" value="php" /> + + <file>.</file> + + <exclude-pattern>tests/javascript/*</exclude-pattern> + <exclude-pattern>*/vendor/*</exclude-pattern> + + <rule ref="Matomo"></rule> + + <rule ref="Generic.Files.LineLength"> + <properties> + <property name="lineLimit" value="250" /> + </properties> + <exclude-pattern>tests/*</exclude-pattern> + </rule> + + <rule ref="Squiz.Classes.ValidClassName.NotCamelCaps"> + <!-- Classnames for our update files don't match PascalCase, this can't be changed easily --> + <exclude-pattern>Updates/*</exclude-pattern> + </rule> + + <rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps"> + <!-- Allow using method name without camel caps in tests as long as some methods are named test_* --> + <exclude-pattern>tests/*</exclude-pattern> + </rule> + + <rule ref="PSR1.Classes.ClassDeclaration.MultipleClasses"> + <!-- Allow using multiple classes in one file for tests --> + <exclude-pattern>tests/*</exclude-pattern> + </rule> +</ruleset> \ No newline at end of file diff --git a/plugins/Bandwidth/phpstan.neon b/plugins/Bandwidth/phpstan.neon new file mode 100644 index 0000000000000000000000000000000000000000..2f5613ba55b1c5c0ce1f3caf06ef7618329859fb --- /dev/null +++ b/plugins/Bandwidth/phpstan.neon @@ -0,0 +1,21 @@ +parameters: + level: 1 + phpVersion: 70200 + tmpDir: /tmp/phpstan/Bandwidth/main + paths: + - . + excludePaths: + - tests/* + - vendor/ + - github-action-tests/ + bootstrapFiles: + - ../../bootstrap-phpstan.php + universalObjectCratesClasses: + - Piwik\Config + - Piwik\View + - Piwik\ViewDataTable\Config + scanDirectories: + # ../../ does not actually seem to give us anything + # that ../plugins/ does not, but including it for + # completeness. It does not seem to slow down performance. + - ../../ diff --git a/plugins/Bandwidth/plugin.json b/plugins/Bandwidth/plugin.json new file mode 100644 index 0000000000000000000000000000000000000000..5bf6aa51752f9c5a7bbaa9ef2a34576a8432b762 --- /dev/null +++ b/plugins/Bandwidth/plugin.json @@ -0,0 +1,27 @@ +{ + "name": "Bandwidth", + "version": "5.0.6", + "description": "Monitor Bandwidth for each page, download, and measure overall traffic in bytes. For requirements check out the description.", + "theme": false, + "keywords": ["bandwidth", "filesize", "size", "download"], + "license": "GPL v3+", + "homepage": "https://matomo.org", + "require": { + "matomo": ">=5.0.0-b4,<6.0.0-b1" + }, + "support": { + "email": "hello@matomo.org", + "issues": "https://github.com/matomo-org/plugin-Bandwidth/issues", + "forum": "https://forum.matomo.org", + "source": "https://github.com/matomo-org/plugin-Bandwidth", + "wiki": "https://github.com/matomo-org/plugin-Bandwidth/wiki" + }, + "authors": [ + { + "name": "Matomo", + "email": "hello@matomo.org", + "homepage": "https://matomo.org" + } + ], + "category": "development" +} diff --git a/plugins/Bandwidth/pull_request_template.md b/plugins/Bandwidth/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..211089bb4ec77dcc05db341c8d1be45bae811eea --- /dev/null +++ b/plugins/Bandwidth/pull_request_template.md @@ -0,0 +1,29 @@ +## Description +<!-- Provide a brief but clear description of the changes introduced by this PR. + Explain WHY the change was necessary and HOW it fixes the issue. + If applicable, mention any dependencies or related changes. --> + +## Issue No +<!-- Mention the issue number this PR addresses. Use GitHub keywords like: + - Fixes #123 (closes the issue when merged) + - Resolves #123 + - Related to #123 (if it doesn’t close the issue) + - JIRA ticket number, if no GitHub issue available +--> + +## Steps to Replicate the Issue +1. <!-- Step 1: Describe the action taken. --> +2. <!-- Step 2: What was the expected result? --> +3. <!-- Step 3: What was the actual result? --> + + + +## Checklist +- [✔/✖] Tested locally or on demo2/demo3? +- [✔/✖/NA] New test case added/updated? +- [✔/✖/NA] Are all newly added texts included via translation? +- [✔/✖/NA] Are text sanitized properly? (Eg use of v-text v/s v-html for vue) +- [✔/✖/NA] Version bumped? +- [✔/✖/NA] I have understood, reviewed, and tested all AI outputs before use +- [✔/✖/NA] All AI instructions respect security, IP, and privacy rules +- [✔/✖/NA] Documentation updated? diff --git a/plugins/Bandwidth/screenshots/Downloads.png b/plugins/Bandwidth/screenshots/Downloads.png new file mode 100644 index 0000000000000000000000000000000000000000..af246d3d163d9d0cc8d0e452a40bae952cf2d5e3 Binary files /dev/null and b/plugins/Bandwidth/screenshots/Downloads.png differ diff --git a/plugins/Bandwidth/screenshots/Pageviews.png b/plugins/Bandwidth/screenshots/Pageviews.png new file mode 100644 index 0000000000000000000000000000000000000000..edab2f6f6a4d5f50a76e30dd1e83da27060fba52 Binary files /dev/null and b/plugins/Bandwidth/screenshots/Pageviews.png differ diff --git a/plugins/Bandwidth/screenshots/Visitor_Overview.png b/plugins/Bandwidth/screenshots/Visitor_Overview.png new file mode 100644 index 0000000000000000000000000000000000000000..420ceecd10289afa6e4b8518ab22254521e9c82d Binary files /dev/null and b/plugins/Bandwidth/screenshots/Visitor_Overview.png differ diff --git a/plugins/Bandwidth/templates/_actionTooltip.twig b/plugins/Bandwidth/templates/_actionTooltip.twig new file mode 100644 index 0000000000000000000000000000000000000000..967215710b34ba8806eeeb27b2f190efbb37191f --- /dev/null +++ b/plugins/Bandwidth/templates/_actionTooltip.twig @@ -0,0 +1,2 @@ + +{{ 'Bandwidth_Bandwidth'|translate }}: {{ action.bandwidth_pretty }} \ No newline at end of file diff --git a/plugins/Bandwidth/templates/_profileSummary.twig b/plugins/Bandwidth/templates/_profileSummary.twig new file mode 100644 index 0000000000000000000000000000000000000000..c97c389a4ff02693e2265088d47d35dc66257542 --- /dev/null +++ b/plugins/Bandwidth/templates/_profileSummary.twig @@ -0,0 +1,13 @@ +<div class="visitor-profile-summary visitor-profile-bandwidth"> + <h1>{{ 'Bandwidth_Bandwidth'|translate }}</h1> + <div> + <p> + {{ 'Bandwidth_SparklineTotalBandwidthDetails'|translate(visitorData.sumBandwidth) }} + ( + {{ 'Bandwidth_ColumnAvgBandwidth'|translate }}: {{ visitorData.avgBandwidth }}, + {{ 'Bandwidth_ColumnMinBandwidth'|translate }}: {{ visitorData.minBandwidth }}, + {{ 'Bandwidth_ColumnMaxBandwidth'|translate }}: {{ visitorData.maxBandwidth }} + ) + </p> + </div> +</div> \ No newline at end of file diff --git a/plugins/Bandwidth/tests/Fixtures/OneVisitWithBandwidth.php b/plugins/Bandwidth/tests/Fixtures/OneVisitWithBandwidth.php new file mode 100644 index 0000000000000000000000000000000000000000..a89ada9900cc582140e0854b1edc6e8186ab0e01 --- /dev/null +++ b/plugins/Bandwidth/tests/Fixtures/OneVisitWithBandwidth.php @@ -0,0 +1,136 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\tests\Fixtures; + +use Piwik\Date; +use Piwik\Filesystem; +use Piwik\Plugin; +use Piwik\Tests\Framework\Fixture; + +/** + * Track actions with bandwidth + */ +class OneVisitWithBandwidth extends Fixture +{ + public $dateTime = '2010-01-03 11:22:33'; + public $idSite = 1; + + /** + * @var \MatomoTracker + */ + private $tracker; + + public function setUp(): void + { + Plugin\Manager::getInstance()->loadPlugin('Bandwidth'); + Plugin\Manager::getInstance()->installLoadedPlugins(); + Filesystem::deleteAllCacheOnUpdate(); + + $this->setUpWebsiteAndUser(); + $this->trackVisits(); + } + + private function setUpWebsiteAndUser() + { + // tests run in UTC, the Tracker in UTC + if (!self::siteCreated($idSite = 1)) { + self::createWebsite($this->dateTime); + } + + if (!self::siteCreated($idSite = 2)) { + self::createWebsite($this->dateTime); + } + + self::createSuperUser(); + } + + public function trackVisits() + { + $this->tracker = self::getTracker($this->idSite, $this->dateTime, $useDefault = true, $uselocal = false); + $this->tracker->setUrl('http://www.example.org/page'); + + $this->trackPageview('Test Title', 550); + $this->trackPageview('Test 2', 200928); + $this->trackPageview('Test Title'); + + $this->moveTimeForward(2); + $this->trackDownload('/test/xyz.png', 23929); + $this->trackDownload('/app.apk', 194948483939); + $this->trackPageview('Test Title 4', 10); + $this->trackPageview('Test Title'); + $this->moveTimeForward(3); + $this->trackPageview('Test 3', 52); + $this->trackPageview('Test Title', 20); + $this->trackPageview('Test 3', 13); + $this->moveTimeForward(4); + $this->trackPageview('Test Title 4', 6889); + $this->trackDownload('/app.apk', 194948483939); + + $this->trackPageview('No bandwidth'); + + $this->moveTimeForward(4.5); + $this->trackPageview('Test Title', 95); + $this->trackPageview('Index', 19493); + $this->trackPageview('Index', 204948283939); + $this->trackPageview('No bandwidth'); + $this->trackPageview('Index', 455); + + + // we want to track pageviews for a different day in the same month year to verify it will still show + // bandwidth columns on that day even there were no bytes tracked but there were some tracked in the same month + $this->tracker = self::getTracker($this->idSite, '2010-01-05 23:23:23', $useDefault = true, $uselocal = false); + $this->trackPageview('Test Title'); + $this->trackPageview('Test Title'); + $this->trackPageview('Test Title 2'); + $this->trackPageview('Index'); + + // we want to track some more pageviews with no bytes to make sure columns are not shown here as there are no + // tracked bytes for this month + $this->tracker = self::getTracker($this->idSite, '2010-02-05 23:23:23', $useDefault = true, $uselocal = false); + $this->trackPageview('Test Title'); + $this->trackPageview('Test Title'); + $this->trackPageview('Index'); + $this->trackDownload('/test/xyz.png'); + $this->trackDownload('/app.apk'); + } + + private function trackPageview($title, $bytes = null) + { + $this->setBandwidthTrackerParam($bytes); + $url = str_replace(' ', '/', strtolower($title)); + $this->tracker->setUrl('http://www.example.org/' . $url); + self::checkResponse($this->tracker->doTrackPageView($title)); + } + + private function trackDownload($path, $bytes = null) + { + $this->setBandwidthTrackerParam($bytes); + self::checkResponse($this->tracker->doTrackAction('http://www.example.org' . $path, 'download')); + } + + private function setBandwidthTrackerParam($bytes) + { + if ($bytes !== null) { + $this->tracker->setDebugStringAppend('bw_bytes=' . $bytes); + } else { + $this->tracker->setDebugStringAppend(''); + } + } + + private function moveTimeForward($minutes) + { + $hour = $minutes / 60; + $this->tracker->setForceVisitDateTime(Date::factory($this->dateTime)->addHour($hour)->getDatetime()); + } + + public function tearDown(): void + { + } +} diff --git a/plugins/Bandwidth/tests/Framework/TestCase/IntegrationTestCase.php b/plugins/Bandwidth/tests/Framework/TestCase/IntegrationTestCase.php new file mode 100644 index 0000000000000000000000000000000000000000..0f295afa87f6b37625ea4ffba70e9f9e52e5f172 --- /dev/null +++ b/plugins/Bandwidth/tests/Framework/TestCase/IntegrationTestCase.php @@ -0,0 +1,122 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\tests\Framework\TestCase; + +use Piwik\DataAccess\ArchiveTableCreator; +use Piwik\Db; +use Piwik\Plugin; +use Piwik\Tests\Framework\Fixture; +use Piwik\Tests\Framework\Mock\FakeAccess; + +/** + * @group Bandwidth + */ +class IntegrationTestCase extends \Piwik\Tests\Framework\TestCase\IntegrationTestCase +{ + protected $date; + + public function setUp(): void + { + parent::setUp(); + $this->setSuperUser(); + + Fixture::createSuperUser(); + Fixture::createWebsite('2014-01-01 00:00:00'); + + Plugin\Manager::getInstance()->loadPlugin('Bandwidth'); + try { + Plugin\Manager::getInstance()->activatePlugin('Bandwidth'); + } catch (\Exception $e) { + } + } + + public function tearDown(): void + { + // clean up your test here if needed + $tables = ArchiveTableCreator::getTablesArchivesInstalled(); + if (!empty($tables)) { + Db::dropTables($tables); + } + parent::tearDown(); + } + + protected function setUser() + { + FakeAccess::$superUser = false; + FakeAccess::$idSitesView = [1]; + FakeAccess::$identity = 'aUser'; + } + + protected function setSuperUser() + { + FakeAccess::$superUser = true; + } + + protected function setAnonymousUser() + { + FakeAccess::clearAccess(); + FakeAccess::$identity = 'anonymous'; + } + + protected function getTracker() + { + $tracker = Fixture::getTracker(1, $this->date . ' 00:01:01', true, true); + $tracker->setTokenAuth(Fixture::getTokenAuth()); + return $tracker; + } + + protected function trackPageviews($bytes) + { + $tracker = $this->getTracker(); + + foreach ($bytes as $byte) { + $this->trackPageview($tracker, $byte); + } + } + + protected function trackPageview($tracker, $byte, $url = null) + { + if (null !== $url) { + $tracker->setUrl('http://www.example.org' . $url); + } + + if (null === $byte) { + $tracker->setDebugStringAppend(''); + } else { + $tracker->setDebugStringAppend('bw_bytes=' . $byte); + } + + $title = $url ?: 'test'; + + $tracker->doTrackPageView($title); + } + + protected function trackDownloads($bytes) + { + $tracker = $this->getTracker(); + + foreach ($bytes as $byte) { + if (null === $byte) { + $tracker->setDebugStringAppend(''); + } else { + $tracker->setDebugStringAppend('bw_bytes=' . $byte); + } + + $tracker->doTrackAction('http://www.example.com/test', 'download'); + } + } + + public function provideContainerConfig() + { + return [ + 'Piwik\Access' => new FakeAccess(), + ]; + } +} diff --git a/plugins/Bandwidth/tests/Integration/APITest.php b/plugins/Bandwidth/tests/Integration/APITest.php new file mode 100644 index 0000000000000000000000000000000000000000..173f21459b98d4353836a0e5b9d6866a8e63fd4f --- /dev/null +++ b/plugins/Bandwidth/tests/Integration/APITest.php @@ -0,0 +1,133 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\tests\Integration; + +use Piwik\DataTable; +use Piwik\Plugins\Bandwidth\API; +use Piwik\Plugins\Bandwidth\Metrics; +use Piwik\Plugins\Bandwidth\tests\Framework\TestCase\IntegrationTestCase; + +/** + * @group Bandwidth + * @group APITest + * @group Plugins + */ +class APITest extends IntegrationTestCase +{ + /** + * @var API + */ + private $api; + + protected $date = '2014-04-04'; + private $idSite = 1; + + public function setUp(): void + { + parent::setUp(); + $this->api = API::getInstance(); + $this->setSuperUser(); + } + + public function test_get_shouldReturnADataTable() + { + $this->trackPageviews([1]); + + $result = $this->api->get($this->idSite, 'month', $this->date); + $result->applyQueuedFilters(); + + $this->assertTrue($result instanceof DataTable); + } + + public function test_get_shouldReturnZero_IfNoTrackedBandwidth() + { + $result = $this->api->get($this->idSite, 'day', $this->date); + + $this->assertTotalBandwidthValue(0, 0, 0, $result); + } + + public function test_get_shouldReturnTheSumOfAllOnlyPageviews() + { + $this->trackPageviews([1, 10, 20, 348]); + $result = $this->api->get($this->idSite, 'day', $this->date); + + $this->assertTotalBandwidthValue(379, 379, 0, $result); + } + + public function test_get_shouldReturnTheSumOfAllOnlyDownloads() + { + $this->trackDownloads([1, 10, 20, 348]); + $result = $this->api->get($this->idSite, 'day', $this->date); + + $this->assertTotalBandwidthValue(379, 0, 379, $result); + } + + public function test_get_shouldReturnTheSumOfAll_MixedPageviewsAndDownloads() + { + $this->trackPageviews([1, 10, 20, 49]); + $this->trackDownloads([59, 4, 1, 34, 592]); + + $result = $this->api->get($this->idSite, 'day', $this->date); + $result->applyQueuedFilters(); + + $this->assertTotalBandwidthValue(770, 80, 690, $result); + } + + public function test_get_shouldReturnTheSumOfAll_DifferentPeriod() + { + $this->trackPageviews([1, 10, 20, 49]); + $this->trackDownloads([59, 4, 1, 34, 592]); + + $result = $this->api->get($this->idSite, 'month', $this->date); + $result->applyQueuedFilters(); + + $this->assertTotalBandwidthValue(770, 80, 690, $result); + } + + public function test_get_shouldReturnFalse_IfColumnShallNotBeDisplayed() + { + $this->trackPageviews([1, 10, 20, 49]); + $this->trackDownloads([59, 4, 1, 34]); + + $result = $this->api->get($this->idSite, 'day', $this->date, false, 'nb_visits'); + $result->applyQueuedFilters(); + + $this->assertTotalBandwidthValue(false, false, false, $result); + } + + public function test_get_shouldReturnSomeColumns_IfValidOnesRequested() + { + $this->trackPageviews([1, 10, 20, 49]); + $this->trackDownloads([59, 4, 1, 34]); + + $displayColumns = Metrics::COLUMN_TOTAL_DOWNLOAD_BANDWIDTH . ',' . Metrics::COLUMN_TOTAL_PAGEVIEW_BANDWIDTH; + $result = $this->api->get($this->idSite, 'day', $this->date, false, $displayColumns); + $result->applyQueuedFilters(); + + $this->assertTotalBandwidthValue(false, 80, 98, $result); + } + + public function test_get_shouldFailIfUserHasNoPermission() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('checkUserHasViewAccess'); + + $this->setAnonymousUser(); + $this->api->get($this->idSite, 'day', $this->date); + } + + private function assertTotalBandwidthValue($expectedOverall, $expectedPageview, $expectedDownload, DataTable $dataTable) + { + $row = $dataTable->getFirstRow(); + $this->assertSame($expectedOverall, $row->getColumn(Metrics::COLUMN_TOTAL_OVERALL_BANDWIDTH)); + $this->assertSame($expectedPageview, $row->getColumn(Metrics::COLUMN_TOTAL_PAGEVIEW_BANDWIDTH)); + $this->assertSame($expectedDownload, $row->getColumn(Metrics::COLUMN_TOTAL_DOWNLOAD_BANDWIDTH)); + } +} diff --git a/plugins/Bandwidth/tests/Integration/BandwidthTest.php b/plugins/Bandwidth/tests/Integration/BandwidthTest.php new file mode 100644 index 0000000000000000000000000000000000000000..081217f966621d8fa58349ecdd618527581dae52 --- /dev/null +++ b/plugins/Bandwidth/tests/Integration/BandwidthTest.php @@ -0,0 +1,190 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\tests\Integration; + +use Piwik\API\Request; +use Piwik\DataTable; +use Piwik\Plugins\Bandwidth\API; +use Piwik\Plugins\Bandwidth\tests\Framework\TestCase\IntegrationTestCase; +use Piwik\Version; + +/** + * Bandidth Class and Bandwidth Tracker test + * + * @group Bandwidth + * @group BandwidthTest + * @group Plugins + */ +class BandwidthTest extends IntegrationTestCase +{ + /** + * @var API + */ + private $api; + + protected $date = '2014-04-04'; + + public function setUp(): void + { + parent::setUp(); + $this->api = API::getInstance(); + + $this->setSuperUser(); + } + + public function test_shouldEnrichPageUrls() + { + $this->trackPageviews([1, 10, null, 5, null, 3949, 399]); + + $result = $this->requestAction('getPageUrls'); + + $row = $result->getFirstRow(); + $this->assertBandwidthStats($row, $maxB = 3949, $minB = 1, $sumB = 4364, $avgB = 872); + } + + public function test_shouldEnrichPageUrls_ZeroShouldCountToAverageCount() + { + $this->trackPageviews([1, 10, 5, 0, null, 3949, 0, null, 399]); + $result = $this->requestAction('getPageUrls'); + + $row = $result->getFirstRow(); + $this->assertSame($row->getColumn('avg_bandwidth'), 623); + } + + public function test_shouldEnrichPageUrls_ShouldNotFailIfNoPageHasBandwidth() + { + $this->trackPageviews([null, null, null, null]); + $result = $this->requestAction('getPageUrls'); + + $row = $result->getFirstRow(); + $this->assertBandwidthStats($row, $maxB = false, $minB = false, $sumB = 0, $avgB = 0); + } + + public function test_shouldEnrichPageUrls_ShouldDefineASegment() + { + $this->trackPageviews([1, 10, 5, 0, null, 3949, 0, null, 399]); + $result = $this->requestAction('getPageUrls', ['segment' => 'bandwidth>=34']); + + $row = $result->getFirstRow(); + + $this->assertBandwidthStats($row, $maxB = 3949, $minB = 0, $sumB = 4364, $avgB = 623); + } + + public function test_shouldEnrichPageTitlesAndFormat_IfRequested() + { + $this->trackPageviews([1, 10, null, 5, null, 3949, 399]); + + $result = $this->requestAction('getPageTitles', ['format_metrics' => '1']); + + $row = $result->getFirstRow(); + $this->assertBandwidthStats($row, $maxB = '3.86 K', $minB = '1 B', $sumB = '4.26 K', $avgB = '872 B'); + } + + public function test_shouldEnrichPageTitles() + { + $this->trackPageviews([1, 10, null, 5, null, 3949, 399]); + + $result = $this->requestAction('getPageTitles'); + + $row = $result->getFirstRow(); + $this->assertBandwidthStats($row, $maxB = 3949, $minB = 1, $sumB = 4364, $avgB = 872); + } + + public function test_shouldEnrichDownloads() + { + $this->trackDownloads([1, 10, null, 5, null, 3949, 397]); + + $result = $this->requestAction('getDownloads'); + + $row = $result->getFirstRow(); + $this->assertBandwidthStats($row, $maxB = 3949, $minB = 1, $sumB = 4362, $avgB = 872); + } + + public function test_shouldEnrichLiveActions() + { + $this->trackPageviews([1, 10, null, 5, null, 3949, 399]); + + $params = [ + 'idSite' => 1, + 'period' => 'day', + 'date' => $this->date, + ]; + + $result = Request::processRequest('Live.getLastVisitsDetails', $params); + $row = $result->getFirstRow(); + + $actions = $row->getColumn('actionDetails'); + foreach ($actions as $action) { + $this->assertArrayHasKey('bandwidth', $action); + } + } + + public function test_manyDifferentUrlsWithFolders_ShouldAggregateStats() + { + $tracker = $this->getTracker(); + $this->trackPageview($tracker, 10, '/index'); + $this->trackPageview($tracker, 20, '/blog/2014/test'); + $this->trackPageview($tracker, 15, '/blog/2014/test2'); + $this->trackPageview($tracker, 3, '/team/contact'); + $this->trackPageview($tracker, null, '/index'); + $this->trackPageview($tracker, 10, '/index'); + + $result = $this->requestAction('getPageUrls'); + $this->assertSame(3, $result->getRowsCount()); + + $row = $result->getRowFromLabel('/index'); + $this->assertBandwidthStats($row, $maxB = 10, $minB = 10, $sumB = 20, $avgB = 10); + $row = $result->getRowFromLabel('team'); + $this->assertBandwidthStats($row, $maxB = 3, $minB = 3, $sumB = 3, $avgB = 3); + $row = $result->getRowFromLabel('blog'); + $this->assertBandwidthStats($row, $maxB = 20, $minB = 15, $sumB = 35, $avgB = 17); + + // request subtable /blog + $result = $this->requestAction('getPageUrls', ['idSubtable' => $row->getIdSubDataTable()]); + $row = $result->getRowFromLabel('2014'); + $this->assertBandwidthStats($row, $maxB = 20, $minB = 15, $sumB = 35, $avgB = 17); + + // request subtable /blog/2014 + $result = $this->requestAction('getPageUrls', ['idSubtable' => $row->getIdSubDataTable()]); + $row = $result->getRowFromLabel('/test'); + $this->assertBandwidthStats($row, $maxB = 20, $minB = 20, $sumB = 20, $avgB = 20); + } + + private function assertBandwidthStats(DataTable\Row $row, $maxB, $minB, $sumB, $avgB) + { + if (version_compare(Version::VERSION, '5.2.0-alpha', '>=')) { + // calculation of min/max metrics was broken before Matomo 5.2, so results would be incorrect + $this->assertSame($row->getColumn('max_bandwidth'), $maxB); + $this->assertSame($row->getColumn('min_bandwidth'), $minB); + } + $this->assertSame($row->getColumn('sum_bandwidth'), $sumB); + $this->assertSame($row->getColumn('avg_bandwidth'), $avgB); + } + + /** + * @param string $action + * @param array $additionalParams + * @return DataTable + */ + private function requestAction($action, $additionalParams = []) + { + $params = [ + 'idSite' => 1, + 'period' => 'day', + 'date' => $this->date, + ]; + + if (!empty($additionalParams)) { + $params = array_merge($params, $additionalParams); + } + + return Request::processRequest('Actions.' . $action, $params); + } +} diff --git a/plugins/Bandwidth/tests/System/CheckDirectDependencyUseCommandTest.php b/plugins/Bandwidth/tests/System/CheckDirectDependencyUseCommandTest.php new file mode 100644 index 0000000000000000000000000000000000000000..9287be2091e52ce463270d20763100fd88f5e75d --- /dev/null +++ b/plugins/Bandwidth/tests/System/CheckDirectDependencyUseCommandTest.php @@ -0,0 +1,48 @@ +<?php + +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\tests\System; + +use Piwik\Plugins\TestRunner\Commands\CheckDirectDependencyUse; +use Piwik\Tests\Framework\TestCase\SystemTestCase; +use Piwik\Version; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\NullOutput; + +class CheckDirectDependencyUseCommandTest extends SystemTestCase +{ + public function testCommand() + { + if (version_compare(Version::VERSION, '5.0.3', '<=') && !file_exists(PIWIK_INCLUDE_PATH . '/plugins/TestRunner/Commands/CheckDirectDependencyUse.php')) { + $this->markTestSkipped('tests:check-direct-dependency-use is not available in this version'); + } + + $pluginName = 'Bandwidth'; + + $checkDirectDependencyUse = new CheckDirectDependencyUse(); + + $console = new \Piwik\Console(self::$fixture->piwikEnvironment); + $console->addCommands([$checkDirectDependencyUse]); + $command = $console->find('tests:check-direct-dependency-use'); + $arguments = [ + 'command' => 'tests:check-direct-dependency-use', + '--plugin' => $pluginName, + '--grep-vendor', + ]; + + $inputObject = new ArrayInput($arguments); + $command->run($inputObject, new NullOutput()); + + $this->assertEquals([ + 'Symfony\Component\Console' => [ + 'Bandwidth/tests/System/CheckDirectDependencyUseCommandTest.php' + ], + ], $checkDirectDependencyUse->usesFoundList[$pluginName]); + } +} diff --git a/plugins/Bandwidth/tests/UI/Bandwidth_spec.js b/plugins/Bandwidth/tests/UI/Bandwidth_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f4c801e6c3da8a3d27d758efc5c125304e04a025 --- /dev/null +++ b/plugins/Bandwidth/tests/UI/Bandwidth_spec.js @@ -0,0 +1,87 @@ +/*! + * Matomo - free/libre analytics platform + * + * Screenshot integration tests. + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe("Bandwidth", function () { + this.timeout(0); + + this.fixture = "Piwik\\Plugins\\Bandwidth\\tests\\Fixtures\\OneVisitWithBandwidth"; + + var generalParams = 'idSite=1&period=day&date=2010-01-03', + secondDateParams = 'idSite=1&period=day&date=2010-01-05', + thirdDateParams = 'idSite=1&period=day&date=2010-02-05', + urlBase = 'module=CoreHome&action=index&' + generalParams; + + before(function () { + testEnvironment.pluginsToLoad = ['Bandwidth']; + testEnvironment.save(); + + }); + + it('should load the actions > pages page correctly', async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=General_Pages&flat=0"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_page_urls'); + }); + + it('should load the actions > pages page flat correctly', async function () { + await page.goto('about:blank'); + await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=General_Pages&flat=1"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_page_urls_flat'); + }); + + it('should load the actions > pages titles correctly', async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=Actions_SubmenuPageTitles"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_page_titles'); + }); + + it('should load the actions > downloads correctly', async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=General_Downloads"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_downloads'); + }); + + it('should load the actions > downloads flat correctly', async function () { + await page.goto('about:blank'); + await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Actions&subcategory=General_Downloads&flat=1"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_downloads_flat'); + }); + + it('should load the visitors > overview correctly', async function () { + await page.goto("?" + urlBase + "#?" + generalParams + "&category=General_Visitors&subcategory=General_Overview&columns=nb_total_overall_bandwidth,nb_total_pageview_bandwidth,nb_total_download_bandwidth"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('visitors_overview'); + }); + + it('should show bandwidth columns if no byte was tracked on that day but during the month', async function () { + await page.goto("?" + urlBase + "#?" + secondDateParams + "&category=General_Actions&subcategory=General_Pages&flat=1"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_no_bandwidth_on_day_but_in_month'); + }); + + it('should not show bandwidth columns if no byte was tracked in actions > pages page', async function () { + await page.goto("?" + urlBase + "#?" + thirdDateParams + "&category=General_Actions&subcategory=General_Pages&flat=1"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_page_urls_no_bandwidth'); + }); + + it('should not show bandwidth columns if no byte was tracked in actions > pages titles', async function () { + await page.goto("?" + urlBase + "#?" + thirdDateParams + "&category=General_Actions&subcategory=Actions_SubmenuPageTitles"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_page_titles_no_bandwidth'); + }); + + it('should not show bandwidth columns if no byte was tracked in actions > downloads', async function () { + await page.goto("?" + urlBase + "#?" + thirdDateParams + "&category=General_Actions&subcategory=General_Downloads"); + var elem = await page.jQuery('.pageWrap'); + expect(await elem.screenshot()).to.matchImage('actions_downloads_no_bandwidth'); + }); +}); \ No newline at end of file diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads.png new file mode 100644 index 0000000000000000000000000000000000000000..be58119b3709ed10a7554025d747f21e76fe4924 Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads_flat.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads_flat.png new file mode 100644 index 0000000000000000000000000000000000000000..038beb9c0165b5d6f6c9e30507ae1c94ec908a85 Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads_flat.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads_no_bandwidth.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads_no_bandwidth.png new file mode 100644 index 0000000000000000000000000000000000000000..e8173be0a68cde9e23a531082b182513f3291340 Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_downloads_no_bandwidth.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_no_bandwidth_on_day_but_in_month.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_no_bandwidth_on_day_but_in_month.png new file mode 100644 index 0000000000000000000000000000000000000000..c2d067ae5198a042266ed5207b5b0d8c54fe1995 Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_no_bandwidth_on_day_but_in_month.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_titles.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_titles.png new file mode 100644 index 0000000000000000000000000000000000000000..c11c89ef4b7438c50a27927f32af3d5471cac352 Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_titles.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_titles_no_bandwidth.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_titles_no_bandwidth.png new file mode 100644 index 0000000000000000000000000000000000000000..7e9cbc97cedb9109e1f3b2e1c52e60b69f166cc9 Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_titles_no_bandwidth.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls.png new file mode 100644 index 0000000000000000000000000000000000000000..c9a5f97ac3ad548c2e400641fedf016c7a9f375d Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls_flat.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls_flat.png new file mode 100644 index 0000000000000000000000000000000000000000..76eaecd8d833861986fbe391a77b7e21145ceae6 Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls_flat.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls_no_bandwidth.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls_no_bandwidth.png new file mode 100644 index 0000000000000000000000000000000000000000..297af9a871448ce0acb2b3187455739b590c6933 Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_actions_page_urls_no_bandwidth.png differ diff --git a/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_visitors_overview.png b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_visitors_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..24793be7e0c7505b67f92061106d845b9b9c5e1d Binary files /dev/null and b/plugins/Bandwidth/tests/UI/expected-ui-screenshots/Bandwidth_visitors_overview.png differ diff --git a/plugins/Bandwidth/tests/Unit/BandwidthTest.php b/plugins/Bandwidth/tests/Unit/BandwidthTest.php new file mode 100644 index 0000000000000000000000000000000000000000..63b7238e122cffe5375dbd10cff0a26cc9009bfa --- /dev/null +++ b/plugins/Bandwidth/tests/Unit/BandwidthTest.php @@ -0,0 +1,66 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\tests\Unit; + +use Piwik\Plugins\Bandwidth\Bandwidth; +use Piwik\Tests\Framework\TestCase\UnitTestCase; + +/** + * @group Bandwidth + * @group BandwidthTest + * @group Plugins + */ +class BandwidthTest extends UnitTestCase +{ + /** + * @var Bandwidth + */ + private $bandwidth; + + public function setUp(): void + { + parent::setUp(); + $this->bandwidth = new Bandwidth(); + } + + public function test_addMetricTranslations_shouldAddToExistTranslations() + { + $translations = [ + 'my' => 'test', + ]; + + $this->bandwidth->addMetricTranslations($translations); + + $this->assertGreaterThanOrEqual(9, count($translations)); + $this->assertSame('test', $translations['my']); // make sure this one is not overwritten + } + + public function test_addActionMetrics_shouldAddToExistingMetrics() + { + $metric = ['aggregation' => 'sum', 'query' => 'count(*)']; + $metrics = [ + 3 => $metric, + ]; + + $this->bandwidth->addActionMetrics($metrics); + + // should have added metrics + $this->assertGreaterThanOrEqual(5, count($metrics)); + + // should still have original metric + $this->assertSame($metric, $metrics[3]); + + // make sure each is properly configured + foreach ($metrics as $metric) { + $this->assertNotEmpty($metric['aggregation']); + $this->assertNotEmpty($metric['query']); + } + } +} diff --git a/plugins/Bandwidth/tests/Unit/MetricsTest.php b/plugins/Bandwidth/tests/Unit/MetricsTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0c6ebcd69f3f78dcd3370148aeb10c86ffdf29e6 --- /dev/null +++ b/plugins/Bandwidth/tests/Unit/MetricsTest.php @@ -0,0 +1,38 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\Bandwidth\tests\Unit; + +use Piwik\Plugins\Bandwidth\Metrics; +use Piwik\Tests\Framework\TestCase\UnitTestCase; + +/** + * @group Bandwidth + * @group MetricsTest + * @group Plugins + */ +class MetricsTest extends UnitTestCase +{ + public function test_getMetricTranslations() + { + $translations = Metrics::getMetricTranslations(); + $expected = [ + 'nb_hits_with_bandwidth' => 'Bandwidth_ColumnHitsWithBandwidth', + 'max_bandwidth' => 'Bandwidth_ColumnMaxBandwidth', + 'min_bandwidth' => 'Bandwidth_ColumnMinBandwidth', + 'sum_bandwidth' => 'Bandwidth_ColumnSumBandwidth', + 'avg_bandwidth' => 'Bandwidth_ColumnAvgBandwidth', + 'nb_total_overall_bandwidth' => 'Bandwidth_ColumnTotalOverallBandwidth', + 'nb_total_pageview_bandwidth' => 'Bandwidth_ColumnTotalPageviewBandwidth', + 'nb_total_download_bandwidth' => 'Bandwidth_ColumnTotalDownloadBandwidth', + ]; + + $this->assertEquals($expected, $translations); + } +} diff --git a/plugins/BotTracking/API.php b/plugins/BotTracking/API.php new file mode 100644 index 0000000000000000000000000000000000000000..207dad852b98fb2097a013d81c1db8191f88ee6a --- /dev/null +++ b/plugins/BotTracking/API.php @@ -0,0 +1,464 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking; + +use Piwik\Archive; +use Piwik\Container\StaticContainer; +use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; +use Piwik\Date; +use Piwik\Piwik; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Plugins\BotTracking\RecordBuilders\AIChatbotReports; +use Piwik\Plugin\ReportsProvider; +use Piwik\Plugins\BotTracking\Reports\Get; +use Piwik\Plugins\Referrers\AIAssistant; +use Piwik\Site; + +/** + * Provides API methods for bot and AI chatbot reporting. + * + * @method static \Piwik\Plugins\BotTracking\API getInstance() + */ +class API extends \Piwik\Plugin\API +{ + public const REAL_TIME_DEFAULT_LOOKBACK_MINUTES = 30; + private const REAL_TIME_MIN_LOOKBACK_MINUTES = 1; + private const REAL_TIME_MAX_LOOKBACK_MINUTES = 720; + + /** + * Returns the main bot tracking report. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param string|string[]|null $columns Optional metric names to include in the report. + * @return DataTable|DataTable\Map Bot tracking metrics for the requested site selection and period. + */ + public function get($idSite, string $period, string $date, $columns = null): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + $archive = Archive::build($idSite, $period, $date, ''); + + $metrics = Metrics::getReportMetricColumns(); + + if ($period !== 'day') { + $metrics = array_filter($metrics, function ($metric) { + return !in_array($metric, [Metrics::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS, Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS]); + }); + } + + $requestedColumns = Piwik::getArrayFromApiParameter($columns); + + /** @var Get $report */ + $report = ReportsProvider::factory('BotTracking', 'get'); + $columns = $report->getMetricsRequiredForReport($metrics, $requestedColumns); + + $dataTable = $archive->getDataTableFromNumeric($columns); + + if (!empty($requestedColumns)) { + $dataTable->queueFilter('ColumnDelete', [$columnsToRemove = [], $requestedColumns]); + } + + return $dataTable; + } + + /** + * Returns a report about AI chatbot requests. + * Depending on the provided secondary dimension the subtables will either contain all requested page urls or document urls. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param bool $expanded Whether subtables should be expanded in the response. + * @param bool $flat Whether subtable rows should be flattened into a single table. + * @param 'pages'|'documents'|null $secondaryDimension Optional secondary dimension for subtable rows. + * Use `pages` for page URLs or `documents` for document URLs. + * @return DataTable|DataTable\Map Requests per AI chatbot for the selected secondary dimension. + */ + public function getAIChatbotRequests($idSite, string $period, string $date, bool $expanded = false, bool $flat = false, ?string $secondaryDimension = null): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + $archiveName = Archiver::AI_CHATBOTS_PAGES_RECORD; + + if ($secondaryDimension === 'documents') { + $archiveName = Archiver::AI_CHATBOTS_DOCUMENTS_RECORD; + } + + $dataTable = Archive::createDataTableFromArchive($archiveName, $idSite, $period, $date, '', $expanded, $flat); + + // When flattening a report, remove all main table rows, where no subtable exists + if ($flat) { + $dataTable->filter(function (DataTable $table) { + foreach ($table->getRows() as $key => $row) { + if (!$row->getIdSubDataTable()) { + $table->deleteRow($key); + } + } + }); + } + + $this->decorateAIChatbotLabels($dataTable); + + return $dataTable; + } + + /** + * Returns AI chatbot activity grouped by chatbot for a real-time lookback window. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * @param int|string $lastMinutes Number of minutes to look back from now. The value must stay + * within the real-time safety window so the API cannot scan too + * much raw data. + * @return DataTable AI chatbot requests, unique page URLs, and error counts for the selected + * real-time lookback window. + */ + public function getAIChatbotsRealTime($idSite, $lastMinutes = self::REAL_TIME_DEFAULT_LOOKBACK_MINUTES): DataTable + { + Piwik::checkUserHasViewAccess($idSite); + + [$startDate, $endDate] = $this->getRealTimeDateRange($lastMinutes); + $idSites = Site::getIdSitesFromIdSitesString($idSite, false, true); + + $table = (new BotRequestsDao())->getAIChatbotActivityForDateRange($idSites, $startDate, $endDate); + + $this->decorateAIChatbotLabels($table); + + return $table; + } + + /** + * Returns page URLs requested by AI chatbots for a real-time lookback window. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * @param int|string $lastMinutes Number of minutes to look back from now. The value must stay + * within the real-time safety window so the API cannot scan too + * much raw data. + * @return DataTable Flat page URL table ordered by chatbot requests for the selected real-time + * lookback window. + */ + public function getTopPageUrlsRealTime($idSite, $lastMinutes = self::REAL_TIME_DEFAULT_LOOKBACK_MINUTES): DataTable + { + Piwik::checkUserHasViewAccess($idSite); + + [$startDate, $endDate] = $this->getRealTimeDateRange($lastMinutes); + $idSites = Site::getIdSitesFromIdSitesString($idSite, false, true); + + $table = (new BotRequestsDao())->getAIChatbotTopPageUrlsForDateRange($idSites, $startDate, $endDate); + + return $this->decorateUrlLabels($table); + } + + /** + * Returns page URLs requested by a specific AI chatbot. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param int $idSubtable Subtable ID for the AI chatbot row to expand. + * @return DataTable|DataTable\Map Page URLs requested by the selected AI chatbot. + */ + public function getPageUrlsForAIChatbot($idSite, string $period, string $date, int $idSubtable): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + return Archive::createDataTableFromArchive(Archiver::AI_CHATBOTS_PAGES_RECORD, $idSite, $period, $date, '', false, false, $idSubtable); + } + + /** + * Returns document URLs requested by a specific AI chatbot. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @param int $idSubtable Subtable ID for the AI chatbot row to expand. + * @return DataTable|DataTable\Map Document URLs requested by the selected AI chatbot. + */ + public function getDocumentUrlsForAIChatbot($idSite, string $period, string $date, int $idSubtable): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + return Archive::createDataTableFromArchive(Archiver::AI_CHATBOTS_DOCUMENTS_RECORD, $idSite, $period, $date, '', false, false, $idSubtable); + } + + /** + * Returns page URLs accessed by AI chatbots across all chatbots, with server time and response size metrics. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @return DataTable|DataTable\Map Flat table of page URLs with Requests, Avg. Server Time, and Avg. Response Size. + */ + public function getAIChatbotContentPages($idSite, string $period, string $date): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + return Archive::createDataTableFromArchive(Archiver::AI_CHATBOTS_REQUESTED_PAGES_RECORD, $idSite, $period, $date, '', false, false); + } + + /** + * Returns document URLs accessed by AI chatbots across all chatbots, with server time and response size metrics. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @return DataTable|DataTable\Map Flat table of document URLs with Requests, Avg. Server Time, and Avg. Response Size. + */ + public function getAIChatbotContentDocuments($idSite, string $period, string $date): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + return Archive::createDataTableFromArchive(Archiver::AI_CHATBOTS_REQUESTED_DOCUMENTS_RECORD, $idSite, $period, $date, '', false, false); + } + + /** + * Returns page and document URLs accessed by AI chatbots that returned HTTP errors (4xx/5xx). + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process, processes data for the period + * containing the specified date. + * @param string $date The date or date range to process. + * 'YYYY-MM-DD', magic keywords (today, yesterday, lastWeek, lastMonth, lastYear), + * or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD', lastX, previousX). + * @return DataTable|DataTable\Map Flat table of broken URLs with 5XX Requests and Page Not Found (404) Requests counts. + */ + public function getAIChatbotBrokenContent($idSite, string $period, string $date): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + return Archive::createDataTableFromArchive(Archiver::AI_CHATBOTS_BROKEN_CONTENT_RECORD, $idSite, $period, $date, '', false, false); + } + + /** + * Returns page URLs visited far more by humans than requested by AI chatbots. + * + * Each row carries Unique Human Pageviews, AI Chatbot Requests and the Human-Favoured + * Discrepancy Score (a bounded 0–100 index materialised on the table). + * + * Note: the "exclude low population" filter that the UI applies by default is a ViewDataTable + * decoration only — a direct API call returns every row (including pages with no human + * pageviews). Segmentation is not supported: any `segment` parameter is ignored and the + * standard, unsegmented data is returned. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process. + * @param string $date The date or date range to process. + * @return DataTable|DataTable\Map Flat table of URLs with the two source metrics and the score. + */ + public function getAIChatbotHumanFavouredPages($idSite, string $period, string $date): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + // The scored data is archived (see AIChatbotFavouredPages); just read it back. The empty + // segment is intentional — these reports are unsegmented, so a requested segment is ignored. + $table = Archive::createDataTableFromArchive(Archiver::AI_CHATBOTS_HUMAN_FAVOURED_PAGES_RECORD, $idSite, $period, $date, '', false, false); + + return $this->skipScoreInReportTotals($table); + } + + /** + * Returns page URLs requested far more by AI chatbots than visited by humans. + * + * Each row carries Unique Human Pageviews, AI Chatbot Requests and the AI-Favoured + * Discrepancy Score (a bounded 0–100 index materialised on the table). + * + * Note: the "exclude low population" filter that the UI applies by default is a ViewDataTable + * decoration only — a direct API call returns every row (including pages with no AI chatbot + * requests). Segmentation is not supported: any `segment` parameter is ignored and the + * standard, unsegmented data is returned. + * + * @param int|string|int[] $idSite Website ID(s) to query. + * - Single site ID (e.g. 1) + * - Multiple site IDs (e.g. [1, 4, 5]) + * - Comma-separated list ("1,4,5") or "all" + * @param 'day'|'week'|'month'|'year'|'range' $period The period to process. + * @param string $date The date or date range to process. + * @return DataTable|DataTable\Map Flat table of URLs with the two source metrics and the score. + */ + public function getAIChatbotAIFavouredPages($idSite, string $period, string $date): DataTableInterface + { + Piwik::checkUserHasViewAccess($idSite); + + // See getAIChatbotHumanFavouredPages: the scored data is archived; read it back unsegmented. + $table = Archive::createDataTableFromArchive(Archiver::AI_CHATBOTS_AI_FAVOURED_PAGES_RECORD, $idSite, $period, $date, '', false, false); + + return $this->skipScoreInReportTotals($table); + } + + /** + * Marks the discrepancy_score column 'skip' so the report totals row leaves it blank — summing a + * per-page 0–100 index is meaningless. The scorer sets this op at archive time, but a DataTable's + * column-aggregation metadata is transient and is not part of the serialised blob, so it is lost on + * load and must be re-applied here on read. Recurses into DataTable\Map (multi-period / multi-site). + * + * @param DataTable|DataTable\Map $table + * @return DataTable|DataTable\Map + */ + private function skipScoreInReportTotals(DataTableInterface $table): DataTableInterface + { + if ($table instanceof DataTable\Map) { + foreach ($table->getDataTables() as $childTable) { + $this->skipScoreInReportTotals($childTable); + } + + return $table; + } + + if ($table instanceof DataTable) { + $ops = $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME); + if (!is_array($ops)) { + $ops = []; + } + $ops[Metrics::COLUMN_DISCREPANCY_SCORE] = 'skip'; + $table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $ops); + } + + return $table; + } + + /** + * @param int|string $lastMinutes + * @return array{0: string, 1: string} + */ + private function getRealTimeDateRange($lastMinutes): array + { + if (!is_int($lastMinutes) && (!is_string($lastMinutes) || !ctype_digit($lastMinutes))) { + throw new \InvalidArgumentException($this->getRealTimeLookbackErrorMessage()); + } + + $lastMinutes = (int) $lastMinutes; + + if ($lastMinutes < self::REAL_TIME_MIN_LOOKBACK_MINUTES || $lastMinutes > self::REAL_TIME_MAX_LOOKBACK_MINUTES) { + throw new \InvalidArgumentException($this->getRealTimeLookbackErrorMessage()); + } + + $now = $this->getRealTimeNowTimestamp(); + + return [ + Date::factory($now - $lastMinutes * 60)->getDatetime(), + Date::factory($now)->getDatetime(), + ]; + } + + private function getRealTimeNowTimestamp(): int + { + try { + $testNow = StaticContainer::get('Tests.now'); + if (!empty($testNow)) { + return (int) $testNow; + } + } catch (\Exception $exception) { + // Tests.now is only available in some test containers. + } + + return Date::getNowTimestamp(); + } + + private function getRealTimeLookbackErrorMessage(): string + { + return sprintf( + 'lastMinutes only accepts values between %d and %d', + self::REAL_TIME_MIN_LOOKBACK_MINUTES, + self::REAL_TIME_MAX_LOOKBACK_MINUTES + ); + } + + private function decorateAIChatbotLabels(DataTableInterface $table): void + { + $table->filter(function (DataTable $table): void { + foreach ($table->getRows() as $row) { + $label = $row->getColumn('label'); + if (!is_string($label)) { + continue; + } + + if (!empty(AIChatbotReports::CHATBOT_MAPPING[$label])) { + $row->setColumn('label', AIChatbotReports::CHATBOT_MAPPING[$label]); + } + } + }); + + $table->queueFilter('ColumnCallbackAddMetadata', [ + 'label', + 'url', + function ($label) { + return AIAssistant::getInstance()->getMainUrlFromName($label); + }, + ]); + $table->queueFilter('MetadataCallbackAddMetadata', [ + 'url', + 'logo', + function ($url) { + return AIAssistant::getInstance()->getLogoFromUrl($url ?: ''); + }, + ]); + } + + private function decorateUrlLabels(DataTable $table): DataTable + { + $table->filter(function (DataTable $table): void { + foreach ($table->getRows() as $row) { + $label = $row->getColumn('label'); + if (is_string($label) && $label !== '') { + $row->setMetadata('url', 'https://' . $label); + } + } + }); + + return $table; + } +} diff --git a/plugins/BotTracking/Archiver.php b/plugins/BotTracking/Archiver.php new file mode 100644 index 0000000000000000000000000000000000000000..14f662167d14f5fe9826fd509872781ae411ebbe --- /dev/null +++ b/plugins/BotTracking/Archiver.php @@ -0,0 +1,28 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking; + +class Archiver extends \Piwik\Plugin\Archiver +{ + public const AI_CHATBOTS_PAGES_RECORD = 'BotTracking_AIChatbotsPages'; + public const AI_CHATBOTS_DOCUMENTS_RECORD = 'BotTracking_AIChatbotsDocuments'; + public const AI_CHATBOTS_REQUESTED_PAGES_RECORD = 'BotTracking_AIChatbotsRequestedPages'; + public const AI_CHATBOTS_REQUESTED_DOCUMENTS_RECORD = 'BotTracking_AIChatbotsRequestedDocuments'; + public const AI_CHATBOTS_BROKEN_CONTENT_RECORD = 'BotTracking_AIChatbotsBrokenContent'; + public const AI_CHATBOTS_HUMAN_FAVOURED_PAGES_RECORD = 'BotTracking_AIChatbotsHumanFavouredPages'; + public const AI_CHATBOTS_AI_FAVOURED_PAGES_RECORD = 'BotTracking_AIChatbotsAIFavouredPages'; + + public static function shouldRunEvenWhenNoVisits(): bool + { + return true; + } +} diff --git a/plugins/BotTracking/BotDetector.php b/plugins/BotTracking/BotDetector.php new file mode 100644 index 0000000000000000000000000000000000000000..38f425442d9fbf85df902bfd279b804d2180e52d --- /dev/null +++ b/plugins/BotTracking/BotDetector.php @@ -0,0 +1,79 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking; + +/** + * Detects AI chatbots and bots based on User-Agent patterns + */ +class BotDetector +{ + public const BOT_TYPE_AI_CHATBOT = 'ai_chatbot'; + + /** @var null|array{bot_name: string, bot_type: string} */ + private $detectionResult; + + /** + * AI Chatbot/Bot User-Agent patterns to detect + * + * @var array<string, string> + */ + private $aiAssistantPatterns = [ + 'ChatGPT-User' => self::BOT_TYPE_AI_CHATBOT, + 'MistralAI-User' => self::BOT_TYPE_AI_CHATBOT, + 'Gemini-Deep-Research' => self::BOT_TYPE_AI_CHATBOT, + 'Claude-User' => self::BOT_TYPE_AI_CHATBOT, + 'Perplexity-User' => self::BOT_TYPE_AI_CHATBOT, + 'Google-NotebookLM' => self::BOT_TYPE_AI_CHATBOT, + ]; + + public function __construct(string $userAgent) + { + $this->detectionResult = $this->detect($userAgent); + } + + /** + * Detect if the User-Agent represents an AI chatbot or bot + * + * @param string $userAgent The User-Agent string to check + * @return null|array{bot_name: string, bot_type: string} Returns array if detected, null otherwise + */ + private function detect(string $userAgent): ?array + { + if (empty($userAgent)) { + return null; + } + + foreach ($this->aiAssistantPatterns as $pattern => $botType) { + if (stripos($userAgent, $pattern) !== false) { + return [ + 'bot_name' => $pattern, + 'bot_type' => $botType, + ]; + } + } + + return null; + } + + public function isBot(): bool + { + return $this->detectionResult !== null; + } + + /** + * @return null|array{'bot_name': string, 'bot_type': string} + */ + public function getDetectionResult(): ?array + { + return $this->detectionResult; + } +} diff --git a/plugins/BotTracking/BotTracking.php b/plugins/BotTracking/BotTracking.php new file mode 100644 index 0000000000000000000000000000000000000000..d7221e88f873bb15aff1a226cfd5ab67009c6c66 --- /dev/null +++ b/plugins/BotTracking/BotTracking.php @@ -0,0 +1,212 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking; + +use Piwik\Date; +use Piwik\Piwik; +use Piwik\Plugin; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; +use Piwik\Plugins\SitesManager\API; +use Piwik\Plugins\BotTracking\Metrics as BotMetrics; +use Piwik\Plugins\BotTracking\Reports\AIChatbotsRealTimeWidgets; +use Piwik\Tracker\Request; + +/** + * BotTracking Plugin + * + * Tracks AI assistant and bot interactions without creating visits. + * Stores telemetry data in dedicated tables for analysis of bot behavior + * and system performance. + */ +class BotTracking extends Plugin +{ + /** + * @return bool + */ + public function isTrackerPlugin() + { + return true; + } + + /** + * @return array<string, string> + */ + public function registerEvents(): array + { + return [ + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'PrivacyManager.deleteLogsOlderThan' => 'deleteLogsOlderThan', + 'PrivacyManager.deleteDataSubjectsForDeletedSites' => 'deleteDataSubjectsForDeletedSites', + 'Tracker.isBotRequest' => 'isBotRequest', + 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + 'Metrics.getEvolutionUnit' => 'getEvolutionUnit', + 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations', + 'Metrics.getDefaultMetricDocumentationTranslations' => 'addMetricDocumentationTranslations', + 'Metrics.getDefaultMetricSemanticTypes' => 'addMetricSemanticTypes', + 'API.addGlossaryItems' => 'addGlossaryItems', + ]; + } + + /** + * @return void + */ + public function install() + { + (new BotRequestsDao())->createTable(); + } + + /** + * @return void + */ + public function uninstall() + { + (new BotRequestsDao())->dropTable(); + } + + public function deleteLogsOlderThan(Date $dateUpperLimit): void + { + (new BotRequestsDao())->deleteOldRecords($dateUpperLimit); + } + + /** + * @param array<string, int> $result + */ + public function deleteDataSubjectsForDeletedSites(array &$result): void + { + $allExistingIdSites = API::getInstance()->getAllSitesId(); + $allExistingIdSites = array_map('intval', $allExistingIdSites); + $maxIdSite = max($allExistingIdSites); + + if (empty($maxIdSite)) { + return; + } + + $dao = new BotRequestsDao(); + $idSitesInTable = $dao->getDistinctIdSitesInTable($maxIdSite); + $idSitesNoLongerExisting = array_diff($idSitesInTable, $allExistingIdSites); + + if (count($idSitesNoLongerExisting) > 0) { + $result[$dao::getTableName()] = $dao->deleteRecordsForIdSites($idSitesNoLongerExisting); + } + } + + /** + * @todo Remove, once Device Detector is able to detect all known ai bots + */ + public function isBotRequest(bool &$isBot, Request $request): void + { + $botDetector = new BotDetector($request->getUserAgent()); + + if ($botDetector->isBot()) { + $isBot = true; + } + } + + public function getEvolutionUnit(?string &$unit, string $column): void + { + if ($column === Metrics::METRIC_AI_CHATBOTS_CLICK_THROUGH_RATE) { + $unit = '%'; + } + } + + /** + * @param array<string, string> $translations + */ + public function addMetricTranslations(array &$translations): void + { + $translations = array_merge($translations, BotMetrics::getMetricTranslations()); + + // Register a default name for the generic 'requests' column used by the new content-URL + // reports. Without this the Glossary majority heuristic renders name = null for the + // 'requests' metric because three reports expose it under different per-report scoped + // documentation strings, leaving no single translation with a majority. Registering the + // default here mirrors the documentation registration in addMetricDocumentationTranslations. + if (!isset($translations[Metrics::COLUMN_REQUESTS])) { + $translations[Metrics::COLUMN_REQUESTS] = Piwik::translate('BotTracking_ColumnRequests'); + } + } + + /** + * @param array<string, string> $translations + */ + public function addMetricDocumentationTranslations(array &$translations): void + { + $translations = array_merge($translations, BotMetrics::getMetricDocumentation()); + + // Register a default documentation for the generic 'requests' column used by the + // new content-URL reports. Without this default the Glossary majority heuristic + // (array_sum - max < max) would not pick a winner because the three reports that + // expose 'requests' each use a different documentation string (generic, page-scoped, + // document-scoped), leaving no single value with a majority. Setting a default here + // ensures the metric always appears in the glossary with the generic tooltip. + if (!isset($translations[Metrics::COLUMN_REQUESTS])) { + $translations[Metrics::COLUMN_REQUESTS] = Piwik::translate('BotTracking_ColumnRequestsDocumentation'); + } + } + + /** + * @param array<string|int, string> $types + */ + public function addMetricSemanticTypes(array &$types): void + { + $types = array_merge($types, BotMetrics::getMetricSemanticTypes()); + } + + /** + * @param array<string, array{title: string, entries: array<int, array<string, string>>}> $glossaryItems + */ + public function addGlossaryItems(array &$glossaryItems): void + { + Piwik::checkUserHasSomeViewAccess(); + + $category = Piwik::translate('General_AIAssistants'); + + foreach (AIChatbotsRealTimeWidgets::getAllWidgets() as $widget) { + $glossaryItems['reports']['entries'][] = [ + 'name' => sprintf('%s (%s)', Piwik::translate($widget['name']), $category), + 'documentation' => Piwik::translate($widget['documentation']), + ]; + } + } + + /** + * @param string[] $translationKeys + * @return void + */ + public function getClientSideTranslationKeys(&$translationKeys) + { + $translationKeys[] = 'BotTracking_DetectingYourSite'; + $translationKeys[] = 'BotTracking_SiteWithoutDataBackToMatomo'; + $translationKeys[] = 'BotTracking_SiteWithoutDataChooseTrackingMethod'; + $translationKeys[] = 'BotTracking_SiteWithoutDataChooseTrackingMethodPreamble1'; + $translationKeys[] = 'BotTracking_SiteWithoutDataChooseTrackingMethodPreamble2'; + $translationKeys[] = 'BotTracking_SiteWithoutDataInstallWithX'; + $translationKeys[] = 'BotTracking_SiteWithoutDataNotYetReady'; + $translationKeys[] = 'BotTracking_SiteWithoutDataOtherInstallMethods'; + $translationKeys[] = 'BotTracking_SiteWithoutDataOtherInstallMethodsIntro'; + $translationKeys[] = 'BotTracking_SiteWithoutDataInstallWithXRecommendation'; + $translationKeys[] = 'BotTracking_SiteWithoutDataRecommendationText'; + $translationKeys[] = 'General_ErrorRequest'; + $translationKeys[] = 'General_Refresh'; + $translationKeys[] = 'Mobile_NavigationBack'; + $translationKeys[] = 'BotTracking_NoRecentAIBotRequests'; + } + + /** + * @param string[] $stylesheets + * @return void + */ + public function getStylesheetFiles(&$stylesheets) + { + $stylesheets[] = "plugins/BotTracking/stylesheets/BotTracking.less"; + } +} diff --git a/plugins/BotTracking/BotTrackingMethod/AmazonCloudFront.php b/plugins/BotTracking/BotTrackingMethod/AmazonCloudFront.php new file mode 100644 index 0000000000000000000000000000000000000000..66b6900a44fb0f8d9e22c70a4e77105510ca38a6 --- /dev/null +++ b/plugins/BotTracking/BotTrackingMethod/AmazonCloudFront.php @@ -0,0 +1,42 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\BotTrackingMethod; + +use Piwik\Plugins\SitesManager\SiteContentDetection\AmazonCloudFront as SitesManagerAmazonCloudFront; +use Piwik\View; + +class AmazonCloudFront extends BotTrackingMethodAbstract +{ + public static function getName(): string + { + return 'Amazon CloudFront'; + } + + public static function getPriority(): int + { + return 50; + } + + public static function getSiteContentDetectionId(): ?string + { + return SitesManagerAmazonCloudFront::getId(); + } + + public static function renderInstructionsTab(): string + { + $view = new View('@BotTracking/_noDataAmazonCloudFront'); + $view->sendHeadersWhenRendering = false; + $view->urlKnowledgeBase = 'https://matomo.org/faq/how-to/install-ai-chatbot-tracking'; + $view->urlSourceCode = 'https://github.com/matomo-org/tracker-cloudfront'; + return $view->render(); + } +} diff --git a/plugins/BotTracking/BotTrackingMethod/BotTrackingMethodAbstract.php b/plugins/BotTracking/BotTrackingMethod/BotTrackingMethodAbstract.php new file mode 100644 index 0000000000000000000000000000000000000000..4fee278f574f577af2a8d12c74bc195f7d2bd131 --- /dev/null +++ b/plugins/BotTracking/BotTrackingMethod/BotTrackingMethodAbstract.php @@ -0,0 +1,44 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\BotTrackingMethod; + +abstract class BotTrackingMethodAbstract +{ + abstract public static function getName(): string; + + abstract public static function getSiteContentDetectionId(): ?string; + + abstract public static function getPriority(): int; + + abstract public static function renderInstructionsTab(): string; + + public static function getLink(): ?string + { + return null; + } + + public static function getIcon(): ?string + { + return null; + } + + public static function getId(): string + { + $classParts = explode('\\', static::class); + return end($classParts); + } + + public static function isOthers(): bool + { + return false; + } +} diff --git a/plugins/BotTracking/BotTrackingMethod/Cloudflare.php b/plugins/BotTracking/BotTrackingMethod/Cloudflare.php new file mode 100644 index 0000000000000000000000000000000000000000..f163378af69db9463b53a68a27d35cb75ff56598 --- /dev/null +++ b/plugins/BotTracking/BotTrackingMethod/Cloudflare.php @@ -0,0 +1,42 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\BotTrackingMethod; + +use Piwik\Plugins\SitesManager\SiteContentDetection\Cloudflare as SitesManagerCloudflare; +use Piwik\View; + +class Cloudflare extends BotTrackingMethodAbstract +{ + public static function getName(): string + { + return 'Cloudflare'; + } + + public static function getPriority(): int + { + return 40; + } + + public static function getSiteContentDetectionId(): ?string + { + return SitesManagerCloudflare::getId(); + } + + public static function renderInstructionsTab(): string + { + $view = new View('@BotTracking/_noDataCloudflare'); + $view->sendHeadersWhenRendering = false; + $view->urlKnowledgeBase = 'https://matomo.org/faq/how-to/install-ai-chatbot-tracking'; + $view->urlSourceCode = 'https://github.com/matomo-org/tracker-cloudflare'; + return $view->render(); + } +} diff --git a/plugins/BotTracking/BotTrackingMethod/HttpTrackingApi.php b/plugins/BotTracking/BotTrackingMethod/HttpTrackingApi.php new file mode 100644 index 0000000000000000000000000000000000000000..3fbe4ebe857219ac3097ea3c5013a1a24217a764 --- /dev/null +++ b/plugins/BotTracking/BotTrackingMethod/HttpTrackingApi.php @@ -0,0 +1,56 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\BotTrackingMethod; + +use Piwik\Piwik; +use Piwik\Url; + +class HttpTrackingApi extends BotTrackingMethodAbstract +{ + public static function getName(): string + { + return Piwik::translate('BotTracking_NoDataHttpTrackingApi'); + } + + public static function getSiteContentDetectionId(): ?string + { + return null; + } + + public static function getPriority(): int + { + return 9000; + } + + public static function getIcon(): ?string + { + return './plugins/SitesManager/images/code.svg'; + } + + public static function isOthers(): bool + { + return true; + } + + public static function renderInstructionsTab(): string + { + $knowledgeBaseLink = Url::getExternalLinkTag('https://matomo.org/docs/tracking-api/'); + $referenceLink = Url::getExternalLinkTag('https://developer.matomo.org/api-reference/tracking-api'); + + $description = Piwik::translate( + 'BotTracking_NoDataHttpTrackingApiDescription', + [$knowledgeBaseLink, '</a>', $referenceLink, '</a>'] + ); + + return '<p>' . $description . '</p>'; + } +} diff --git a/plugins/BotTracking/BotTrackingMethod/MissingOneContactUs.php b/plugins/BotTracking/BotTrackingMethod/MissingOneContactUs.php new file mode 100644 index 0000000000000000000000000000000000000000..7302956601f34231af8c1260cbaed140b730f684 --- /dev/null +++ b/plugins/BotTracking/BotTrackingMethod/MissingOneContactUs.php @@ -0,0 +1,48 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\BotTrackingMethod; + +use Piwik\Piwik; +use Piwik\Url; + +class MissingOneContactUs extends BotTrackingMethodAbstract +{ + public static function getName(): string + { + return Piwik::translate('BotTracking_NoDataMissingOneContactUs'); + } + + public static function getSiteContentDetectionId(): ?string + { + return null; + } + + public static function getPriority(): int + { + return 10000; + } + + public static function getIcon(): ?string + { + return './plugins/SitesManager/images/others.svg'; + } + + public static function renderInstructionsTab(): string + { + return ''; + } + + public static function getLink(): ?string + { + return Url::addCampaignParametersToMatomoLink('https://matomo.org/support/'); + } +} diff --git a/plugins/BotTracking/BotTrackingMethod/WordPress.php b/plugins/BotTracking/BotTrackingMethod/WordPress.php new file mode 100644 index 0000000000000000000000000000000000000000..badf8c0d1116f5f8582f9ea17516476238307ee9 --- /dev/null +++ b/plugins/BotTracking/BotTrackingMethod/WordPress.php @@ -0,0 +1,42 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\BotTrackingMethod; + +use Piwik\Plugins\SitesManager\SiteContentDetection\WordPress as SitesManagerWordPress; +use Piwik\View; + +class WordPress extends BotTrackingMethodAbstract +{ + public static function getName(): string + { + return 'WordPress'; + } + + public static function getPriority(): int + { + return 30; + } + + public static function getSiteContentDetectionId(): ?string + { + return SitesManagerWordPress::getId(); + } + + public static function renderInstructionsTab(): string + { + $view = new View('@BotTracking/_noDataWordPress'); + $view->sendHeadersWhenRendering = false; + $view->urlKnowledgeBase = 'https://matomo.org/faq/how-to/install-ai-chatbot-tracking-wordpress'; + $view->urlSourceCode = 'https://github.com/matomo-org/matomo-for-wordpress'; + return $view->render(); + } +} diff --git a/plugins/BotTracking/Categories/AIChatbotsContentRequestsSubcategory.php b/plugins/BotTracking/Categories/AIChatbotsContentRequestsSubcategory.php new file mode 100644 index 0000000000000000000000000000000000000000..99d839172df7c665517752679d6c9e098b4e0330 --- /dev/null +++ b/plugins/BotTracking/Categories/AIChatbotsContentRequestsSubcategory.php @@ -0,0 +1,32 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Categories; + +use Piwik\Category\Subcategory; +use Piwik\Piwik; + +class AIChatbotsContentRequestsSubcategory extends Subcategory +{ + protected $categoryId = 'General_AIAssistants'; + protected $id = 'BotTracking_AIChatbotsContentRequests'; + protected $order = 15; + + public function getHelp() + { + return sprintf( + '<p>%1$s</p><p>%2$s</p><p>%3$s</p>', + Piwik::translate('BotTracking_AIChatbotsContentRequestsHelp1'), + Piwik::translate('BotTracking_AIChatbotsContentRequestsHelp2'), + Piwik::translate('BotTracking_AIChatbotsContentRequestsHelp3') + ); + } +} diff --git a/plugins/BotTracking/Categories/AIChatbotsOverviewSubcategory.php b/plugins/BotTracking/Categories/AIChatbotsOverviewSubcategory.php new file mode 100644 index 0000000000000000000000000000000000000000..5257f48e333bedf4a2d496b8602ba8a910d4b745 --- /dev/null +++ b/plugins/BotTracking/Categories/AIChatbotsOverviewSubcategory.php @@ -0,0 +1,32 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Categories; + +use Piwik\Category\Subcategory; +use Piwik\Piwik; + +class AIChatbotsOverviewSubcategory extends Subcategory +{ + protected $categoryId = 'General_AIAssistants'; + protected $id = 'BotTracking_AIChatbotsOverview'; + protected $order = 10; + + public function getHelp() + { + return sprintf( + '<p>%1$s</p><p>%2$s</p><p>%3$s</p>', + Piwik::translate('BotTracking_AIChatbotsOverviewHelp1'), + Piwik::translate('BotTracking_AIChatbotsOverviewHelp2'), + Piwik::translate('BotTracking_AIChatbotsOverviewHelp3') + ); + } +} diff --git a/plugins/BotTracking/Categories/AIChatbotsRealtimeSubcategory.php b/plugins/BotTracking/Categories/AIChatbotsRealtimeSubcategory.php new file mode 100644 index 0000000000000000000000000000000000000000..5b0a94f135f5136c981cfb17fd631285adada4b0 --- /dev/null +++ b/plugins/BotTracking/Categories/AIChatbotsRealtimeSubcategory.php @@ -0,0 +1,31 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Categories; + +use Piwik\Category\Subcategory; +use Piwik\Piwik; + +class AIChatbotsRealtimeSubcategory extends Subcategory +{ + protected $categoryId = 'General_AIAssistants'; + protected $id = 'BotTracking_AIChatbotsRealtime'; + protected $order = 12; + + public function getHelp() + { + return sprintf( + '<p>%1$s</p><p>%2$s</p>', + Piwik::translate('BotTracking_AIChatbotsRealtimeHelp1'), + Piwik::translate('BotTracking_AIChatbotsRealtimeHelp2') + ); + } +} diff --git a/plugins/BotTracking/Columns/AIChatbotName.php b/plugins/BotTracking/Columns/AIChatbotName.php new file mode 100644 index 0000000000000000000000000000000000000000..b908cdd4f5d7389953d40abe900db92ac00aebc9 --- /dev/null +++ b/plugins/BotTracking/Columns/AIChatbotName.php @@ -0,0 +1,20 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns; + +use Piwik\Columns\Dimension; + +class AIChatbotName extends Dimension +{ + protected $nameSingular = 'BotTracking_ColumnAIChatbotName'; + protected $type = self::TYPE_TEXT; +} diff --git a/plugins/BotTracking/Columns/ContentUrl.php b/plugins/BotTracking/Columns/ContentUrl.php new file mode 100644 index 0000000000000000000000000000000000000000..413b5e34b4f2374b8c979bceedcc568b44d5cecf --- /dev/null +++ b/plugins/BotTracking/Columns/ContentUrl.php @@ -0,0 +1,25 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns; + +use Piwik\Columns\Dimension; + +/** + * Neutral "URL" dimension used by the Broken Content report which mixes page and document URLs. + * Unlike PageUrl (TYPE_PAGE_URL only) and DocumentUrl (TYPE_DOWNLOAD only), this dimension + * carries no action-type filter so the column header fits a mixed page + document URL list. + */ +class ContentUrl extends Dimension +{ + protected $nameSingular = 'BotTracking_ContentUrl'; + protected $type = self::TYPE_URL; +} diff --git a/plugins/BotTracking/Columns/DocumentUrl.php b/plugins/BotTracking/Columns/DocumentUrl.php new file mode 100644 index 0000000000000000000000000000000000000000..063fe9fa68b7e559fdcfa3d631c2123090f0b85f --- /dev/null +++ b/plugins/BotTracking/Columns/DocumentUrl.php @@ -0,0 +1,20 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns; + +use Piwik\Columns\Dimension; + +class DocumentUrl extends Dimension +{ + protected $nameSingular = 'BotTracking_DocumentUrl'; + protected $type = self::TYPE_URL; +} diff --git a/plugins/BotTracking/Columns/Metrics/AIChatbotRequests.php b/plugins/BotTracking/Columns/Metrics/AIChatbotRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..58cc8c4286b8d47dfdc9507e6e760d967cf58936 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/AIChatbotRequests.php @@ -0,0 +1,59 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +/** + * Column metric for the AI-chatbot request count on the Human/AI-Favoured Pages reports. + * + * Extends AggregatedMetric purely to supply the column's name/translation/format/semantic-type + * metadata. The value is materialised during archiving by + * {@see \Piwik\Plugins\BotTracking\RecordBuilders\AIChatbotFavouredPages} (the AI chatbot + * page-request count). AggregatedMetric is simply the closest base for "a plain column that + * isn't a processed metric". + */ +class AIChatbotRequests extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_AI_CHATBOT_REQUESTS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnAIChatbotRequests'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnAIChatbotRequestsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/AcquiredVisits.php b/plugins/BotTracking/Columns/Metrics/AcquiredVisits.php new file mode 100644 index 0000000000000000000000000000000000000000..2de5f74ccda122b1dd9df779891900395e67c09b --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/AcquiredVisits.php @@ -0,0 +1,50 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class AcquiredVisits extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_ACQUIRED_VISITS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnAcquiredVisits'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnAcquiredVisitsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/AvgResponseSize.php b/plugins/BotTracking/Columns/Metrics/AvgResponseSize.php new file mode 100644 index 0000000000000000000000000000000000000000..11e191dbf6b74bb696960575d34727d6d59813b5 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/AvgResponseSize.php @@ -0,0 +1,74 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\DataTable\Row; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\ProcessedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class AvgResponseSize extends ProcessedMetric +{ + public function getName() + { + return Metrics::COLUMN_AVG_RESPONSE_SIZE; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnAvgResponseSize'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnAvgResponseSizeDocumentation'); + } + + public function compute(Row $row) + { + $rawSum = $this->getMetric($row, Metrics::COLUMN_SUM_RESPONSE_SIZE); + $rawNb = $this->getMetric($row, Metrics::COLUMN_NB_RESPONSE_SIZE); + + $sum = is_numeric($rawSum) ? (int) $rawSum : 0; + $nb = is_numeric($rawNb) ? (int) $rawNb : 0; + + if (empty($nb)) { + return false; + } + + return (float)($sum / $nb); + } + + public function getDependentMetrics() + { + return [ + Metrics::COLUMN_SUM_RESPONSE_SIZE, + Metrics::COLUMN_NB_RESPONSE_SIZE, + ]; + } + + /** + * @param float|int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettySizeFromBytes($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_BYTE; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/AvgServerTime.php b/plugins/BotTracking/Columns/Metrics/AvgServerTime.php new file mode 100644 index 0000000000000000000000000000000000000000..101760ea316a8dc9a7631a188ba4cf8b64791bb8 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/AvgServerTime.php @@ -0,0 +1,75 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\DataTable\Row; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\ProcessedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class AvgServerTime extends ProcessedMetric +{ + public function getName() + { + return Metrics::COLUMN_AVG_SERVER_TIME; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnAvgServerTime'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnAvgServerTimeDocumentation'); + } + + public function compute(Row $row) + { + $rawSum = $this->getMetric($row, Metrics::COLUMN_SUM_SERVER_TIME); + $rawNb = $this->getMetric($row, Metrics::COLUMN_NB_SERVER_TIME); + + $sum = is_numeric($rawSum) ? (int) $rawSum : 0; + $nb = is_numeric($rawNb) ? (int) $rawNb : 0; + + if (empty($nb)) { + return false; + } + + // Stored values are in milliseconds; divide by 1000 to return seconds. + return (float)($sum / $nb / 1000); + } + + public function getDependentMetrics() + { + return [ + Metrics::COLUMN_SUM_SERVER_TIME, + Metrics::COLUMN_NB_SERVER_TIME, + ]; + } + + /** + * @param float|int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyTimeFromSeconds($value, true); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_DURATION_S; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/ClickThroughRate.php b/plugins/BotTracking/Columns/Metrics/ClickThroughRate.php new file mode 100644 index 0000000000000000000000000000000000000000..91f3ab82e26bafae56a2e0368c3912a645c1ea2c --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/ClickThroughRate.php @@ -0,0 +1,70 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\DataTable\Row; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\ProcessedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class ClickThroughRate extends ProcessedMetric +{ + public function getName() + { + return Metrics::METRIC_AI_CHATBOTS_CLICK_THROUGH_RATE; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnClickThroughRate'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnClickThroughRateDocumentation'); + } + + public function getDependentMetrics() + { + return [ + Metrics::METRIC_AI_CHATBOTS_REQUESTS, + Metrics::METRIC_AI_CHATBOTS_ACQUIRED_VISITS, + ]; + } + + public function compute(Row $row) + { + $rawRequests = $this->getMetric($row, Metrics::METRIC_AI_CHATBOTS_REQUESTS); + $rawVisits = $this->getMetric($row, Metrics::METRIC_AI_CHATBOTS_ACQUIRED_VISITS); + + $requests = is_numeric($rawRequests) ? (int) $rawRequests : 0; + $visits = is_numeric($rawVisits) ? (int) $rawVisits : 0; + + return Piwik::getQuotientSafe($visits, $requests, 4); + } + + /** + * @param number $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyPercentFromQuotient($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_PERCENT; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/DiscrepancyScore.php b/plugins/BotTracking/Columns/Metrics/DiscrepancyScore.php new file mode 100644 index 0000000000000000000000000000000000000000..3b754eb41f2ac181028ba2eabc84dc889b41fdd0 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/DiscrepancyScore.php @@ -0,0 +1,78 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use InvalidArgumentException; +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +/** + * Column metadata (name / translation / per-variant documentation / formatting) for the bounded + * 0–100 Discrepancy Score on the Human/AI-Favoured Pages reports. The value is materialised during + * archiving by {@see \Piwik\Plugins\BotTracking\RecordBuilders\AIChatbotFavouredPages} (via + * {@see \Piwik\Plugins\BotTracking\DataTable\FavouredPagesScorer}). AggregatedMetric is the closest + * base for "a plain column that isn't a processed metric" (as for the two source-metric columns). + */ +class DiscrepancyScore extends AggregatedMetric +{ + public const VARIANT_HUMAN_FAVOURED = 'human_favoured'; + public const VARIANT_AI_FAVOURED = 'ai_favoured'; + + /** @var self::VARIANT_HUMAN_FAVOURED|self::VARIANT_AI_FAVOURED */ + private $variant; + + public function __construct(string $variant) + { + if ($variant !== self::VARIANT_HUMAN_FAVOURED && $variant !== self::VARIANT_AI_FAVOURED) { + throw new InvalidArgumentException( + 'Unknown DiscrepancyScore variant: ' . $variant + ); + } + $this->variant = $variant; + } + + public function getName() + { + return Metrics::COLUMN_DISCREPANCY_SCORE; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnDiscrepancyScore'); + } + + public function getDocumentation() + { + $key = $this->variant === self::VARIANT_HUMAN_FAVOURED + ? 'BotTracking_ColumnDiscrepancyScoreHumanFavouredDocumentation' + : 'BotTracking_ColumnDiscrepancyScoreAIFavouredDocumentation'; + + return Piwik::translate($key); + } + + /** + * @param float|int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber((float) $value, 1); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/DocumentRequests.php b/plugins/BotTracking/Columns/Metrics/DocumentRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..62b1106b6d59d1a9c4ef3a037ca56efdaba264d9 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/DocumentRequests.php @@ -0,0 +1,50 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class DocumentRequests extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_DOCUMENT_REQUESTS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnDocumentRequests'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnDocumentRequestsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/PageNotFound404Requests.php b/plugins/BotTracking/Columns/Metrics/PageNotFound404Requests.php new file mode 100644 index 0000000000000000000000000000000000000000..c80616d27f44b3886f39afada8999930c8c886b6 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/PageNotFound404Requests.php @@ -0,0 +1,50 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class PageNotFound404Requests extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_PAGE_NOT_FOUND_404_REQUESTS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnPageNotFound404Requests'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnPageNotFound404RequestsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/PageRequests.php b/plugins/BotTracking/Columns/Metrics/PageRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..982bf374b44e7afba85eb80b85571c5949684c90 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/PageRequests.php @@ -0,0 +1,50 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class PageRequests extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_PAGE_REQUESTS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnPageRequests'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnPageRequestsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/Requests.php b/plugins/BotTracking/Columns/Metrics/Requests.php new file mode 100644 index 0000000000000000000000000000000000000000..c333a75d0ce0338306ef10762c37073c113a345f --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/Requests.php @@ -0,0 +1,50 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class Requests extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_REQUESTS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnRequests'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnRequestsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/ServerError5xxRequests.php b/plugins/BotTracking/Columns/Metrics/ServerError5xxRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..402e6a0894e4319241594020444fd743112cb556 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/ServerError5xxRequests.php @@ -0,0 +1,50 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class ServerError5xxRequests extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_SERVER_ERROR_5XX_REQUESTS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnServerError5xxRequests'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnServerError5xxRequestsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/TotalBrokenRequests.php b/plugins/BotTracking/Columns/Metrics/TotalBrokenRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..30e44db56ed8ba890987cf9e05328b8c84928d3e --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/TotalBrokenRequests.php @@ -0,0 +1,50 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +class TotalBrokenRequests extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_TOTAL_BROKEN_REQUESTS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnTotalBrokenRequests'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnTotalBrokenRequestsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/Metrics/UniqueHumanPageviews.php b/plugins/BotTracking/Columns/Metrics/UniqueHumanPageviews.php new file mode 100644 index 0000000000000000000000000000000000000000..d92dcee1ad5ad5b6297705f12b682909df4b3368 --- /dev/null +++ b/plugins/BotTracking/Columns/Metrics/UniqueHumanPageviews.php @@ -0,0 +1,59 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns\Metrics; + +use Piwik\Columns\Dimension; +use Piwik\Metrics\Formatter; +use Piwik\Piwik; +use Piwik\Plugin\AggregatedMetric; +use Piwik\Plugins\BotTracking\Metrics; + +/** + * Column metric for the human-side pageview count on the Human/AI-Favoured Pages reports. + * + * Extends AggregatedMetric purely to supply the column's name/translation/format/semantic-type + * metadata. The value is materialised during archiving by + * {@see \Piwik\Plugins\BotTracking\RecordBuilders\AIChatbotFavouredPages} (human pageviews = + * distinct visits per page URL, matching the Actions Pages report's nb_visits). AggregatedMetric is + * simply the closest base for "a plain column that isn't a processed metric". + */ +class UniqueHumanPageviews extends AggregatedMetric +{ + public function getName() + { + return Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS; + } + + public function getTranslatedName() + { + return Piwik::translate('BotTracking_ColumnUniqueHumanPageviews'); + } + + public function getDocumentation() + { + return Piwik::translate('BotTracking_ColumnUniqueHumanPageviewsDocumentation'); + } + + /** + * @param int $value + * @return string + */ + public function format($value, Formatter $formatter) + { + return $formatter->getPrettyNumber($value); + } + + public function getSemanticType(): ?string + { + return Dimension::TYPE_NUMBER; + } +} diff --git a/plugins/BotTracking/Columns/PageUrl.php b/plugins/BotTracking/Columns/PageUrl.php new file mode 100644 index 0000000000000000000000000000000000000000..cf6515736e8a8a0c6705eb7a81d1d51e0505b9e0 --- /dev/null +++ b/plugins/BotTracking/Columns/PageUrl.php @@ -0,0 +1,20 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Columns; + +use Piwik\Columns\Dimension; + +class PageUrl extends Dimension +{ + protected $nameSingular = 'BotTracking_PageUrl'; + protected $type = self::TYPE_URL; +} diff --git a/plugins/BotTracking/Controller.php b/plugins/BotTracking/Controller.php new file mode 100644 index 0000000000000000000000000000000000000000..d3a6b24b0ea18d7022c47670ed1ba65162bef630 --- /dev/null +++ b/plugins/BotTracking/Controller.php @@ -0,0 +1,212 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking; + +use Piwik\Container\StaticContainer; +use Piwik\DataTable\Renderer\Json; +use Piwik\Piwik; +use Piwik\Plugins\BotTracking\BotTrackingMethod\BotTrackingMethodAbstract; +use Piwik\Plugins\BotTracking\Reports\SegmentNotSupportedMessageHelper; +use Piwik\Plugin\Menu; +use Piwik\Plugin\Manager; +use Piwik\Request; +use Piwik\SiteContentDetector; +use Piwik\Url; + +class Controller extends \Piwik\Plugin\Controller +{ + public function siteWithoutData(): string + { + $this->checkSitePermission(); + + $request = Request::fromRequest(); + $period = $request->getStringParameter('period', ''); + $date = $request->getStringParameter('date', ''); + + return $this->renderTemplateAs('siteWithoutData', [ + 'backToMatomoLink' => $this->buildAIBotsOverviewUrl($period, $date), + 'hideWhatIsNew' => true, + ], $viewType = 'basic'); + } + + public function getTrackingMethodsForSite(): string + { + $this->checkSitePermission(); + + $siteContentDetector = $this->buildSiteContentDetector(); + $trackingMethodClasses = $this->getBotTrackingMethods(); + $detectContent = []; + + foreach ($trackingMethodClasses as $trackingMethodClass) { + $contentDetectionId = $trackingMethodClass::getSiteContentDetectionId(); + if ($contentDetectionId !== null && $contentDetectionId !== '') { + $detectContent[] = $contentDetectionId; + } + } + + $siteContentDetector->detectContent($detectContent, $this->idSite); + + $trackingMethods = []; + + foreach ($trackingMethodClasses as $trackingMethodClass) { + $contentDetectionId = $trackingMethodClass::getSiteContentDetectionId(); + $contentDetectionClass = null; + $wasDetected = false; + + if (!empty($contentDetectionId)) { + $contentDetectionClass = $siteContentDetector->getSiteContentDetectionById($contentDetectionId); + + if (null === $contentDetectionClass) { + continue; + } + + $wasDetected = $siteContentDetector->wasDetected($contentDetectionId); + } + + $tabContent = $trackingMethodClass::renderInstructionsTab(); + $link = $trackingMethodClass::getLink(); + $icon = $trackingMethodClass::getIcon(); + + if (!empty($tabContent) || !empty($link)) { + if (empty($icon) && $contentDetectionClass) { + $icon = $contentDetectionClass->getIcon(); + } + + $trackingMethods[] = [ + 'id' => $trackingMethodClass::getId(), + 'name' => $trackingMethodClass::getName(), + 'content' => $tabContent, + 'icon' => $icon ?? '', + 'isOthers' => $trackingMethodClass::isOthers(), + 'priority' => $trackingMethodClass::getPriority(), + 'wasDetected' => $wasDetected, + 'link' => $link, + ]; + } + } + + usort($trackingMethods, function ($a, $b) { + if ($a['wasDetected'] === $b['wasDetected']) { + return $a['priority'] === $b['priority'] ? 0 : ($a['priority'] < $b['priority'] ? -1 : 1); + } + + return $a['wasDetected'] ? -1 : 1; + }); + + Json::sendHeaderJSON(); + return json_encode(['trackingMethods' => $trackingMethods]); + } + + public function getEvolutionGraph(): ?string + { + $this->checkSitePermission(); + + $columns = []; + + $columnsFromRequest = Request::fromRequest()->getParameter('columns', ''); + if (!empty($columnsFromRequest) && (is_array($columnsFromRequest) || is_string($columnsFromRequest))) { + $columns = Piwik::getArrayFromApiParameter($columnsFromRequest); + } + + $documentation = Piwik::translate('BotTracking_ChatbotsOverTimeReportDocumentation') . '<br /><br />'; + $translations = Metrics::getMetricTranslations(); + $docs = Metrics::getMetricDocumentation(); + foreach (Metrics::getSparklineMetricOrder() as $metric) { + if (empty($translations[$metric]) || empty($docs[$metric])) { + continue; + } + + $documentation .= sprintf('<b>%s:</b> %s<br />', $translations[$metric], $docs[$metric]); + } + + $metrics = Metrics::getSparklineMetricOrder(); + + if (Request::fromRequest()->getStringParameter('period', '') !== 'day') { + $metrics = array_filter($metrics, function ($metric) { + return !in_array($metric, [Metrics::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS, Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS]); + }); + } + + $view = $this->getLastUnitGraphAcrossPlugins( + $this->pluginName, + __FUNCTION__, + $columns, + $metrics, + $documentation, + 'BotTracking.get' + ); + + if (empty($view->config->columns_to_display)) { + $view->config->columns_to_display = [Metrics::METRIC_AI_CHATBOTS_REQUESTS]; + } + + SegmentNotSupportedMessageHelper::addSegmentNotSupportedMessage($view); + + return $this->renderView($view); + } + + public function showNoRecentRequestsMessage(): string + { + $this->checkSitePermission(); + + $request = Request::fromRequest(); + $period = $request->getStringParameter('period', ''); + $date = $request->getStringParameter('date', ''); + + Json::sendHeaderJSON(); + + return json_encode( + NoRecentRequestsMessage::shouldShow($this->idSite, $period, $date) + ); + } + + /** + * @return array<class-string<BotTrackingMethodAbstract>> + */ + protected function getBotTrackingMethods(): array + { + return Manager::getInstance()->findMultipleComponents( + 'BotTrackingMethod', + BotTrackingMethodAbstract::class + ); + } + + protected function buildSiteContentDetector(): SiteContentDetector + { + return StaticContainer::get(SiteContentDetector::class); + } + + protected function buildAIBotsOverviewUrl(string $period, string $date): string + { + $menu = new Menu(); + $params = $menu->urlForDefaultUserParams($this->idSite); + $params['module'] = 'CoreHome'; + $params['action'] = 'index'; + + if ($period !== '') { + $params['period'] = $period; + } + + if ($date !== '') { + $params['date'] = $date; + } + + $hashParams = $params; + $hashParams['category'] = 'General_AIAssistants'; + $hashParams['subcategory'] = 'BotTracking_AIChatbotsOverview'; + + return 'index.php?' + . Url::getQueryStringFromParameters($params) + . '#?' + . Url::getQueryStringFromParameters($hashParams); + } +} diff --git a/plugins/BotTracking/Dao/BotRequestsDao.php b/plugins/BotTracking/Dao/BotRequestsDao.php new file mode 100644 index 0000000000000000000000000000000000000000..24133c5864a8453483ba2a7d3eb3140fb7fc2438 --- /dev/null +++ b/plugins/BotTracking/Dao/BotRequestsDao.php @@ -0,0 +1,357 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Dao; + +use Piwik\Common; +use Piwik\Config\GeneralConfig; +use Piwik\DataTable; +use Piwik\DataTable\Row; +use Piwik\Date; +use Piwik\Db; +use Piwik\DbHelper; +use Piwik\Plugins\BotTracking\BotDetector; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Tracker\Action; + +class BotRequestsDao +{ + private const CONFIG_LIVE_AI_CHATBOTS_MAXIMUM_ROWS = 'live_ai_chatbots_maximum_rows'; + private const CONFIG_LIVE_AI_CHATBOTS_TOP_PAGE_URLS_MAXIMUM_ROWS = 'live_ai_chatbots_top_page_urls_maximum_rows'; + + public static function getTableName(): string + { + return 'log_bot_request'; + } + + public static function getPrefixedTableName(): string + { + return Common::prefixTable(self::getTableName()); + } + + public static function getAIChatbotActivityForDateRangeLimit(): int + { + return self::getRealTimeReportLimit(self::CONFIG_LIVE_AI_CHATBOTS_MAXIMUM_ROWS); + } + + public static function getAIChatbotTopPageUrlsForDateRangeLimit(): int + { + return self::getRealTimeReportLimit(self::CONFIG_LIVE_AI_CHATBOTS_TOP_PAGE_URLS_MAXIMUM_ROWS); + } + + /** + * Creates the log table + */ + public function createTable(): void + { + $tableName = self::getTableName(); + $definition = ' + `idrequest` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `idsite` INT UNSIGNED NOT NULL, + `server_time` DATETIME NOT NULL, + `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `idaction_url` INT UNSIGNED NULL, + `bot_name` VARCHAR(100) NOT NULL, + `bot_type` VARCHAR(50) NOT NULL, + `http_status_code` SMALLINT UNSIGNED NULL, + `response_size_bytes` INT UNSIGNED NULL, + `response_time_ms` INT UNSIGNED NULL, + `source` VARCHAR(50) NULL, + PRIMARY KEY (`idrequest`), + INDEX `index_idsite_server_time` (`idsite`, `server_time`)'; + + DbHelper::createTable($tableName, $definition); + } + + public function dropTable(): void + { + Db::query('DROP TABLE IF EXISTS ' . self::getPrefixedTableName()); + } + + /** + * Insert a bot telemetry record + * + * @param array<string, scalar> $data The telemetry data to insert + * @return int The inserted record ID + */ + public function insert(array $data): int + { + $tableName = self::getPrefixedTableName(); + + $fields = [ + 'idsite', + 'server_time', + 'idaction_url', + 'bot_name', + 'bot_type', + 'http_status_code', + 'response_size_bytes', + 'response_time_ms', + 'source', + ]; + + $values = []; + $bind = []; + + foreach ($fields as $field) { + if (isset($data[$field])) { + $values[] = '?'; + $bind[] = $data[$field]; + } else { + $values[] = 'NULL'; + } + } + + $sql = sprintf( + 'INSERT INTO `%s` (%s) VALUES (%s)', + $tableName, + implode(', ', $fields), + implode(', ', $values) + ); + + Db::query($sql, $bind); + + return (int)Db::get()->lastInsertId(); + } + + /** + * Delete bot telemetry records older than a specified date + * + * @param Date $date Delete records older than this date + * @return int Number of deleted records + */ + public function deleteOldRecords(Date $date): int + { + $tableName = self::getPrefixedTableName(); + + $sql = sprintf( + 'DELETE FROM `%s` WHERE server_time < ?', + $tableName + ); + + $result = Db::query($sql, [$date->getDatetime()]); + + return (int)Db::get()->rowCount($result); + } + + /** + * Delete bot telemetry records for specific sites + * + * @param array<int|string> $siteIds Delete records older than this date + * @return int Number of deleted records + */ + public function deleteRecordsForIdSites(array $siteIds): int + { + $tableName = self::getPrefixedTableName(); + $siteIds = array_map('intval', $siteIds); + + $sql = sprintf( + 'DELETE FROM `%s` WHERE idsite IN (' . implode(', ', $siteIds) . ') LIMIT 25000', + $tableName + ); + + $result = Db::query($sql); + + return (int)Db::get()->rowCount($result); + } + + public function getLastServerTimeForSiteAndBotType(int $idSite, string $botType): ?string + { + $tableName = self::getPrefixedTableName(); + + return Db::fetchOne( + sprintf( + 'SELECT MAX(server_time) FROM `%s` WHERE idsite = ? AND bot_type = ?', + $tableName + ), + [$idSite, $botType] + ); + } + + /** + * @param int[] $idSites + */ + public function getAIChatbotActivityForDateRange(array $idSites, string $startDate, string $endDate): DataTable + { + $idSites = $this->normalizeIdSites($idSites); + + if (empty($idSites)) { + return new DataTable(); + } + + $limit = self::getAIChatbotActivityForDateRangeLimit(); + + $idSitePlaceholders = Common::getSqlStringFieldsArray($idSites); + $tableName = self::getPrefixedTableName(); + $actionTable = Common::prefixTable('log_action'); + + $sql = sprintf( + "SELECT bot.bot_name AS label, + COUNT(*) AS %s, + COUNT(DISTINCT CASE WHEN log_action.type = %d THEN log_action.name END) AS %s, + SUM(CASE WHEN bot.http_status_code IN (404, 410) THEN 1 ELSE 0 END) AS %s, + SUM(CASE WHEN bot.http_status_code BETWEEN 500 AND 599 THEN 1 ELSE 0 END) AS %s + FROM `%s` AS bot + LEFT JOIN `%s` AS log_action ON log_action.idaction = bot.idaction_url + WHERE bot.idsite IN (%s) + AND bot.bot_type = ? + AND bot.server_time >= ? + AND bot.server_time <= ? + GROUP BY bot.bot_name + ORDER BY %s DESC, bot.bot_name + LIMIT %d", + Metrics::COLUMN_REQUESTS, + Action::TYPE_PAGE_URL, + Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS, + Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS, + Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS, + $tableName, + $actionTable, + $idSitePlaceholders, + Metrics::COLUMN_REQUESTS, + $limit + ); + + $bind = array_merge($idSites, [BotDetector::BOT_TYPE_AI_CHATBOT, $startDate, $endDate]); + + $sql = $this->addRealTimeQueryMaxExecutionTimeHint($sql); + + $stmt = Db::query($sql, $bind); + $table = new DataTable(); + $table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, [ + Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS => 'skip', + ]); + + while ($row = $stmt->fetch()) { + /** @var array<string, int|string|null> $row */ + $table->addRow(new Row([ + Row::COLUMNS => [ + 'label' => (string) $row['label'], + Metrics::COLUMN_REQUESTS => (int) $row[Metrics::COLUMN_REQUESTS], + Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS => (int) $row[Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS], + Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS => (int) $row[Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS], + Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS => (int) $row[Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS], + ], + ])); + } + + return $table; + } + + /** + * @param int[] $idSites + */ + public function getAIChatbotTopPageUrlsForDateRange(array $idSites, string $startDate, string $endDate): DataTable + { + $idSites = $this->normalizeIdSites($idSites); + + if (empty($idSites)) { + return new DataTable(); + } + + $limit = self::getAIChatbotTopPageUrlsForDateRangeLimit(); + + $idSitePlaceholders = Common::getSqlStringFieldsArray($idSites); + $tableName = self::getPrefixedTableName(); + $actionTable = Common::prefixTable('log_action'); + + $sql = sprintf( + "SELECT log_action.name AS label, + COUNT(*) AS %s + FROM `%s` AS bot + INNER JOIN `%s` AS log_action ON log_action.idaction = bot.idaction_url + WHERE bot.idsite IN (%s) + AND bot.bot_type = ? + AND bot.server_time >= ? + AND bot.server_time <= ? + AND log_action.name IS NOT NULL + AND log_action.name <> '' + AND log_action.type = %d + GROUP BY log_action.name + ORDER BY %s DESC, log_action.name + LIMIT %d", + Metrics::COLUMN_REQUESTS, + $tableName, + $actionTable, + $idSitePlaceholders, + Action::TYPE_PAGE_URL, + Metrics::COLUMN_REQUESTS, + $limit + ); + + $bind = array_merge($idSites, [BotDetector::BOT_TYPE_AI_CHATBOT, $startDate, $endDate]); + + $sql = $this->addRealTimeQueryMaxExecutionTimeHint($sql); + + $stmt = Db::query($sql, $bind); + $table = new DataTable(); + + while ($row = $stmt->fetch()) { + /** @var array{label: string, requests: int|string} $row */ + $table->addRow(new Row([ + Row::COLUMNS => [ + 'label' => (string) $row['label'], + Metrics::COLUMN_REQUESTS => (int) $row[Metrics::COLUMN_REQUESTS], + ], + ])); + } + + return $table; + } + + /** + * @return int[] + */ + public function getDistinctIdSitesInTable(int $maxIdSite): array + { + $tableName = self::getPrefixedTableName(); + $idSitesLogTable = Db::fetchAll('SELECT DISTINCT idsite FROM ' . $tableName); + $idSitesLogTable = array_column($idSitesLogTable, 'idsite'); + $idSitesLogTable = array_map('intval', $idSitesLogTable); + return array_filter($idSitesLogTable, function ($idSite) use ($maxIdSite) { + return !empty($idSite) && $idSite <= $maxIdSite; + }); + } + + /** + * @param int[] $idSites + * @return int[] + */ + private function normalizeIdSites(array $idSites): array + { + $idSites = array_map('intval', $idSites); + $idSites = array_filter($idSites, function (int $idSite): bool { + return $idSite > 0; + }); + + return array_values(array_unique($idSites)); + } + + private function addRealTimeQueryMaxExecutionTimeHint(string $sql): string + { + return DbHelper::addMaxExecutionTimeHintToQuery($sql, $this->getRealTimeQueryMaxExecutionTime()); + } + + private static function getRealTimeReportLimit(string $configKey): int + { + $limit = GeneralConfig::getIntegerConfigValue($configKey); + if ($limit === null || $limit <= 0) { + throw new \UnexpectedValueException(sprintf('Config option "%s" must be greater than 0.', $configKey)); + } + + return $limit; + } + + private function getRealTimeQueryMaxExecutionTime(): float + { + return GeneralConfig::getFloatConfigValue('live_query_max_execution_time', 0.0); + } +} diff --git a/plugins/BotTracking/DataTable/FavouredPagesScorer.php b/plugins/BotTracking/DataTable/FavouredPagesScorer.php new file mode 100644 index 0000000000000000000000000000000000000000..5c69ea495daa993d531006484cef8356070e466f --- /dev/null +++ b/plugins/BotTracking/DataTable/FavouredPagesScorer.php @@ -0,0 +1,135 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\DataTable; + +use InvalidArgumentException; +use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; +use Piwik\Plugins\BotTracking\Columns\Metrics\DiscrepancyScore; +use Piwik\Plugins\BotTracking\Metrics; + +/** + * Materialises the bounded 0–100 Discrepancy Score as a real column on the merged favoured-pages + * table. Invoked during archiving by {@see \Piwik\Plugins\BotTracking\RecordBuilders\AIChatbotFavouredPages} + * — for the day record and, re-run, for each higher period's recomputed union. + * + * A stored column rather than a ProcessedMetric: the score is table-relative (the `volume` term is + * anchored to the busiest page), so a ProcessedMetric recomputed at display time would see a shrunken + * anchor once ExcludeLowPopulation has deleted rows (and be absent while that filter itself runs). + * + * @see DiscrepancyScore for the column metadata (label, documentation, formatting). + */ +class FavouredPagesScorer +{ + /** @var DiscrepancyScore::VARIANT_HUMAN_FAVOURED|DiscrepancyScore::VARIANT_AI_FAVOURED */ + private $variant; + + public function __construct(string $variant) + { + if ( + $variant !== DiscrepancyScore::VARIANT_HUMAN_FAVOURED + && $variant !== DiscrepancyScore::VARIANT_AI_FAVOURED + ) { + throw new InvalidArgumentException('Unknown DiscrepancyScore variant: ' . $variant); + } + $this->variant = $variant; + } + + /** + * Adds the `discrepancy_score` column to every row of the table (recursing into + * `DataTable\Map` children so each period self-calibrates), and marks the column 'skip' so it is + * never summed into a summary row. + * + * @param DataTable|DataTable\Map $table + */ + public function addScores(DataTableInterface $table): void + { + if ($table instanceof DataTable\Map) { + foreach ($table->getDataTables() as $childTable) { + $this->addScores($childTable); + } + return; + } + + if (!$table instanceof DataTable) { + return; + } + + $strongColumn = $this->variant === DiscrepancyScore::VARIANT_HUMAN_FAVOURED + ? Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS + : Metrics::COLUMN_AI_CHATBOT_REQUESTS; + + // Volume anchor = the busiest page's strong-side value. Exclude the "Others" summary row, + // whose aggregate tail would otherwise hijack the anchor and deflate every real score. + $maxStrong = 0; + foreach ($table->getRows() as $row) { + if ($row->isSummaryRow()) { + continue; + } + $value = (int) $row->getColumn($strongColumn); + if ($value > $maxStrong) { + $maxStrong = $value; + } + } + + foreach ($table->getRows() as $row) { + // Leave the "Others" summary row unscored — a score over an aggregate of URLs is meaningless. + if ($row->isSummaryRow()) { + continue; + } + + $human = (int) $row->getColumn(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS); + $ai = (int) $row->getColumn(Metrics::COLUMN_AI_CHATBOT_REQUESTS); + + if ($this->variant === DiscrepancyScore::VARIANT_HUMAN_FAVOURED) { + $strong = $human; + $weak = $ai; + } else { + $strong = $ai; + $weak = $human; + } + + $row->setColumn(Metrics::COLUMN_DISCREPANCY_SCORE, self::score($strong, $weak, $maxStrong)); + } + + // Mark the score 'skip' so the Truncate filter leaves it out of the "Others" summary row it + // builds while archiving — a score over an aggregate of URLs is meaningless. (Not serialised + // with the blob, so readers re-apply 'skip' for the report totals row; see API.) + $ops = $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME); + if (!is_array($ops)) { + $ops = []; + } + $ops[Metrics::COLUMN_DISCREPANCY_SCORE] = 'skip'; + $table->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $ops); + } + + /** + * The bounded 0–100 score for one page: + * lean = max(0, (strong − weak) / (strong + weak)) // 0 = balanced or opposite, 1 = entirely one-sided + * volume = log10(strong + 1) / log10(maxStrong + 1) // 0..1, anchored to the busiest page + * score = round(100 × lean × volume, 1) + */ + public static function score(int $strong, int $weak, int $maxStrong): float + { + $total = $strong + $weak; + if ($total <= 0) { + return 0.0; + } + + $lean = max(0, ($strong - $weak) / $total); + + $anchor = log10($maxStrong + 1); + $volume = $anchor > 0 ? log10($strong + 1) / $anchor : 0.0; + + return round(100 * $lean * $volume, 1); + } +} diff --git a/plugins/BotTracking/Metrics.php b/plugins/BotTracking/Metrics.php new file mode 100644 index 0000000000000000000000000000000000000000..a7ef9056d7fee23ae66c9e7efd42acdeda484fb2 --- /dev/null +++ b/plugins/BotTracking/Metrics.php @@ -0,0 +1,142 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking; + +use Piwik\Piwik; +use Piwik\Columns\Dimension; + +class Metrics +{ + public const METRIC_AI_CHATBOTS_REQUESTS = 'BotTracking_AIChatbotsRequests'; + public const METRIC_AI_CHATBOTS_ACQUIRED_VISITS = 'BotTracking_AIChatbotsAcquiredVisits'; + public const METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS = 'BotTracking_AIChatbotsUniquePageUrls'; + public const METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS = 'BotTracking_AIChatbotsUniqueDocumentUrls'; + public const METRIC_AI_CHATBOTS_UNIQUE_CHATBOTS = 'BotTracking_AIChatbotsUniqueChatbots'; + public const METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS = 'BotTracking_AIChatbotsNotFoundRequests'; + public const METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS = 'BotTracking_AIChatbotsServerErrorRequests'; + public const METRIC_AI_CHATBOTS_CLICK_THROUGH_RATE = 'BotTracking_AIChatbotsClickThroughRate'; + public const COLUMN_REQUESTS = 'requests'; + public const COLUMN_DOCUMENT_REQUESTS = 'document_requests'; + public const COLUMN_PAGE_REQUESTS = 'page_requests'; + public const COLUMN_ACQUIRED_VISITS = 'visits_acquired'; + public const COLUMN_AVG_SERVER_TIME = 'avg_server_time'; + public const COLUMN_AVG_RESPONSE_SIZE = 'avg_response_size'; + public const COLUMN_PAGE_NOT_FOUND_404_REQUESTS = 'page_not_found_404_requests'; + public const COLUMN_SERVER_ERROR_5XX_REQUESTS = 'server_error_5xx_requests'; + public const COLUMN_TOTAL_BROKEN_REQUESTS = 'total_broken_requests'; + + // Columns for the Human/AI-Favoured Pages reports — computed at API time, not archived. + public const COLUMN_UNIQUE_HUMAN_PAGEVIEWS = 'unique_human_pageviews'; + public const COLUMN_AI_CHATBOT_REQUESTS = 'ai_chatbot_requests'; + public const COLUMN_DISCREPANCY_SCORE = 'discrepancy_score'; + + // Raw (blob-stored) accumulator columns used to compute the above averages at display time. + public const COLUMN_SUM_SERVER_TIME = 'sum_server_time'; + public const COLUMN_NB_SERVER_TIME = 'nb_server_time'; + public const COLUMN_SUM_RESPONSE_SIZE = 'sum_response_size'; + public const COLUMN_NB_RESPONSE_SIZE = 'nb_response_size'; + + + // ----------------------------------------------------------------------------------------- + // Legacy translation/doc/semantic maps — AI Chatbots Overview numerics only. + // New per-column metrics (COLUMN_AVG_*, COLUMN_*_5XX_REQUESTS, COLUMN_TOTAL_BROKEN_REQUESTS, …) + // live on their Metric classes — do NOT add them here. + // ----------------------------------------------------------------------------------------- + + /** + * Metrics displayed in the reports (including derived metrics). + * + * @return string[] + */ + public static function getReportMetricColumns(): array + { + return [ + self::METRIC_AI_CHATBOTS_REQUESTS, + self::METRIC_AI_CHATBOTS_ACQUIRED_VISITS, + self::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS, + self::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS, + self::METRIC_AI_CHATBOTS_UNIQUE_CHATBOTS, + self::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS, + self::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS, + ]; + } + + /** + * Ordered list used for sparklines & graph selectors. + * + * @return string[] + */ + public static function getSparklineMetricOrder(): array + { + return [ + self::METRIC_AI_CHATBOTS_REQUESTS, + self::METRIC_AI_CHATBOTS_ACQUIRED_VISITS, + self::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS, + self::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS, + self::METRIC_AI_CHATBOTS_CLICK_THROUGH_RATE, + self::METRIC_AI_CHATBOTS_UNIQUE_CHATBOTS, + self::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS, + self::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS, + ]; + } + + /** + * @return array<string, string> + */ + public static function getMetricTranslations(): array + { + return [ + self::METRIC_AI_CHATBOTS_REQUESTS => Piwik::translate('BotTracking_ColumnRequests'), + self::METRIC_AI_CHATBOTS_ACQUIRED_VISITS => Piwik::translate('BotTracking_ColumnAcquiredVisits'), + self::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS => Piwik::translate('BotTracking_ColumnUniquePageUrls'), + self::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS => Piwik::translate('BotTracking_ColumnUniqueDocumentUrls'), + self::METRIC_AI_CHATBOTS_UNIQUE_CHATBOTS => Piwik::translate('BotTracking_ColumnUniqueAiChatbots'), + self::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS => Piwik::translate('BotTracking_ColumnNotFoundRequests'), + self::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS => Piwik::translate('BotTracking_ColumnServerErrorRequests'), + self::METRIC_AI_CHATBOTS_CLICK_THROUGH_RATE => Piwik::translate('BotTracking_ColumnClickThroughRate'), + ]; + } + + /** + * @return array<string, string> + */ + public static function getMetricDocumentation(): array + { + return [ + self::METRIC_AI_CHATBOTS_REQUESTS => Piwik::translate('BotTracking_ColumnRequestsDocumentation'), + self::METRIC_AI_CHATBOTS_ACQUIRED_VISITS => Piwik::translate('BotTracking_ColumnAcquiredVisitsDocumentation'), + self::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS => Piwik::translate('BotTracking_ColumnUniquePageUrlsDocumentation'), + self::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS => Piwik::translate('BotTracking_ColumnUniqueDocumentUrlsDocumentation'), + self::METRIC_AI_CHATBOTS_UNIQUE_CHATBOTS => Piwik::translate('BotTracking_ColumnUniqueAiChatbotsDocumentation'), + self::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS => Piwik::translate('BotTracking_ColumnNotFoundRequestsDocumentation'), + self::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS => Piwik::translate('BotTracking_ColumnServerErrorRequestsDocumentation'), + self::METRIC_AI_CHATBOTS_CLICK_THROUGH_RATE => Piwik::translate('BotTracking_ColumnClickThroughRateDocumentation'), + ]; + } + + /** + * @return array<string, string> + */ + public static function getMetricSemanticTypes(): array + { + return [ + self::METRIC_AI_CHATBOTS_REQUESTS => Dimension::TYPE_NUMBER, + self::METRIC_AI_CHATBOTS_ACQUIRED_VISITS => Dimension::TYPE_NUMBER, + self::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS => Dimension::TYPE_NUMBER, + self::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS => Dimension::TYPE_NUMBER, + self::METRIC_AI_CHATBOTS_UNIQUE_CHATBOTS => Dimension::TYPE_NUMBER, + self::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS => Dimension::TYPE_NUMBER, + self::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS => Dimension::TYPE_NUMBER, + self::METRIC_AI_CHATBOTS_CLICK_THROUGH_RATE => Dimension::TYPE_PERCENT, + ]; + } +} diff --git a/plugins/BotTracking/NoRecentRequestsMessage.php b/plugins/BotTracking/NoRecentRequestsMessage.php new file mode 100644 index 0000000000000000000000000000000000000000..d40dc91fe0843c4eaf70ec2d7fb2467fa9b4a5f7 --- /dev/null +++ b/plugins/BotTracking/NoRecentRequestsMessage.php @@ -0,0 +1,63 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking; + +use Piwik\Date; +use Piwik\Period; +use Piwik\Site; +use Piwik\Plugins\BotTracking\BotDetector; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; + +class NoRecentRequestsMessage +{ + public static function shouldShow(int $idSite, string $period, string $date): bool + { + if ($idSite <= 0 || $period === '' || $date === '') { + return false; + } + + try { + $selected = Period\Factory::build($period, $date); + } catch (\Exception $e) { + return false; + } + + $siteTimezone = Site::getTimezoneFor($idSite); + $todayLocal = Date::factoryInTimezone('today', $siteTimezone) + ->setTimezone($siteTimezone); + + $last7Start = $todayLocal->subDay(6); + $last7End = $todayLocal->getEndOfDay(); + $selectedStart = $selected->getDateStart()->setTimezone($siteTimezone); + $selectedEnd = $selected->getDateEnd()->getEndOfDay()->setTimezone($siteTimezone); + + $overlapsLast7 = !$selectedEnd->isEarlier($last7Start) + && !$selectedStart->isLater($last7End); + + if (!$overlapsLast7) { + return false; + } + + // server_time is stored in UTC and is indexed with idsite, so querying the latest value is fast. + $lastRequest = (new BotRequestsDao())->getLastServerTimeForSiteAndBotType( + $idSite, + BotDetector::BOT_TYPE_AI_CHATBOT + ); + + if (empty($lastRequest)) { + return true; + } + + // Date::factory interprets the stored UTC string, so comparison is done in UTC timestamps. + return Date::factory($lastRequest)->isEarlier($last7Start); + } +} diff --git a/plugins/BotTracking/RecordBuilders/AIChatbotContentReports.php b/plugins/BotTracking/RecordBuilders/AIChatbotContentReports.php new file mode 100644 index 0000000000000000000000000000000000000000..15ddcff24cffdbf17fef856859f3cb1e69ec1fd5 --- /dev/null +++ b/plugins/BotTracking/RecordBuilders/AIChatbotContentReports.php @@ -0,0 +1,135 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\RecordBuilders; + +use Piwik\ArchiveProcessor; +use Piwik\ArchiveProcessor\Record; +use Piwik\ArchiveProcessor\RecordBuilder; +use Piwik\Common; +use Piwik\Config\GeneralConfig; +use Piwik\DataTable; +use Piwik\Plugins\BotTracking\Archiver; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Tracker\Action; + +/** + * Builds three flat blob records for the "AI Chatbots Content Requests" subcategory: + * - Pages record: page URLs requested by AI chatbots, with request counts and avg server-time/response-size. + * - Documents record: download URLs requested by AI chatbots, with the same metrics. + * - Broken Content record: page + document URLs that returned HTTP 4xx/5xx errors, with per-status counts. + * All three records are keyed by full URL (single label dimension); no URL-path subtables. + * + * The per-URL page/document request query lives in {@see AIChatbotPageMetricsTrait} so the Favoured Pages + * builder can reuse the exact same "AI chatbot request" counting logic. + */ +class AIChatbotContentReports extends RecordBuilder +{ + use AIChatbotPageMetricsTrait; + + /** + * @var int + */ + protected $maxRowsInTable; + + /** + * @var int + */ + private $rankingQueryLimit; + + public function __construct() + { + parent::__construct(); + + $this->maxRowsInTable = GeneralConfig::getIntegerConfigValue('datatable_archiving_maximum_rows_ai_chatbot_content', 50000); + $this->rankingQueryLimit = $this->getRankingQueryLimit($this->maxRowsInTable); + } + + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + return [ + Record::make(Record::TYPE_BLOB, Archiver::AI_CHATBOTS_REQUESTED_PAGES_RECORD) + ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_REQUESTS), + Record::make(Record::TYPE_BLOB, Archiver::AI_CHATBOTS_REQUESTED_DOCUMENTS_RECORD) + ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_REQUESTS), + Record::make(Record::TYPE_BLOB, Archiver::AI_CHATBOTS_BROKEN_CONTENT_RECORD) + ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_TOTAL_BROKEN_REQUESTS), + ]; + } + + public function isEnabled(ArchiveProcessor $archiveProcessor): bool + { + // don't process reports for any segment + return $archiveProcessor->getParams()->getSegment()->isEmpty(); + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + // The content reports expose the full metric set (requests + avg server-time/response-size + errors). + $columns = array_keys($this->pageUrlMetricExpressions()); + + $tables = [ + Archiver::AI_CHATBOTS_REQUESTED_PAGES_RECORD => $this->queryPageOrDocumentUrls($archiveProcessor, Action::TYPE_PAGE_URL, $this->rankingQueryLimit, $columns), + Archiver::AI_CHATBOTS_REQUESTED_DOCUMENTS_RECORD => $this->queryPageOrDocumentUrls($archiveProcessor, Action::TYPE_DOWNLOAD, $this->rankingQueryLimit, $columns), + Archiver::AI_CHATBOTS_BROKEN_CONTENT_RECORD => $this->queryBrokenContent($archiveProcessor), + ]; + + return $tables; + } + + /** + * Queries page and document URLs that returned HTTP errors (404/410/5xx) to AI chatbots. + * The HAVING clause in the inner query ensures only URLs with at least one error row appear. + */ + private function queryBrokenContent(ArchiveProcessor $archiveProcessor): DataTable + { + $logAggregator = $archiveProcessor->getLogAggregator(); + $where = $logAggregator->getWhereStatement('bot', 'server_time'); + $botTable = BotRequestsDao::getPrefixedTableName(); + $actionTable = Common::prefixTable('log_action'); + + $innerSql = sprintf( + "SELECT log_action.name AS url, + SUM(bot.http_status_code IN (404, 410)) + SUM(bot.http_status_code BETWEEN 500 AND 599) AS %s, + SUM(bot.http_status_code IN (404, 410)) AS %s, + SUM(bot.http_status_code BETWEEN 500 AND 599) AS %s + FROM `%s` AS bot + INNER JOIN `%s` AS log_action ON log_action.idaction = bot.idaction_url + WHERE log_action.name IS NOT NULL + AND log_action.name <> '' + AND log_action.type IN (%d, %d) + AND bot.bot_type = ? + AND %s + GROUP BY log_action.name + HAVING %s >= 1 + ORDER BY %s DESC", + Metrics::COLUMN_TOTAL_BROKEN_REQUESTS, + Metrics::COLUMN_PAGE_NOT_FOUND_404_REQUESTS, + Metrics::COLUMN_SERVER_ERROR_5XX_REQUESTS, + $botTable, + $actionTable, + Action::TYPE_PAGE_URL, + Action::TYPE_DOWNLOAD, + $where, + Metrics::COLUMN_TOTAL_BROKEN_REQUESTS, + Metrics::COLUMN_TOTAL_BROKEN_REQUESTS + ); + + $columns = [ + Metrics::COLUMN_TOTAL_BROKEN_REQUESTS => 'sum', + Metrics::COLUMN_PAGE_NOT_FOUND_404_REQUESTS => 'sum', + Metrics::COLUMN_SERVER_ERROR_5XX_REQUESTS => 'sum', + ]; + + return $this->executeUrlQuery($archiveProcessor, $innerSql, $columns, $this->rankingQueryLimit); + } +} diff --git a/plugins/BotTracking/RecordBuilders/AIChatbotFavouredPages.php b/plugins/BotTracking/RecordBuilders/AIChatbotFavouredPages.php new file mode 100644 index 0000000000000000000000000000000000000000..2299bc21efbba5acaf65dcc1a019a8b78e359a8c --- /dev/null +++ b/plugins/BotTracking/RecordBuilders/AIChatbotFavouredPages.php @@ -0,0 +1,293 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\RecordBuilders; + +use Piwik\ArchiveProcessor; +use Piwik\ArchiveProcessor\Record; +use Piwik\ArchiveProcessor\RecordBuilder; +use Piwik\Common; +use Piwik\Config\GeneralConfig; +use Piwik\DataTable; +use Piwik\DataTable\Row; +use Piwik\Db; +use Piwik\Plugins\BotTracking\Archiver; +use Piwik\Plugins\BotTracking\Columns\Metrics\DiscrepancyScore; +use Piwik\Plugins\BotTracking\DataTable\FavouredPagesScorer; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\RankingQuery; +use Piwik\Tracker\Action; + +/** + * Builds the two flat blob records backing the Human-Favoured and AI-Favoured Pages reports, keyed by + * page URL with `unique_human_pageviews`, `ai_chatbot_requests` and `discrepancy_score`. + * + * Both sides are aggregated from the log tables on the same `log_action.name`, so the union is an + * exact label match. The 0–100 {@see DiscrepancyScore} is materialised per variant and used as the + * truncation sort, so each record keeps the pages that rank highest for its own report. + * + * One record per variant (~2x storage) is needed because the score is variant-specific: a single + * record truncated by one variant's score would drop the other variant's most-relevant rows. + */ +class AIChatbotFavouredPages extends RecordBuilder +{ + use AIChatbotPageMetricsTrait; + + /** + * @var int + */ + protected $maxRowsInTable; + + /** + * @var int + */ + private $rankingQueryLimit; + + public function __construct() + { + parent::__construct(); + + $this->maxRowsInTable = GeneralConfig::getIntegerConfigValue('datatable_archiving_maximum_rows_ai_chatbot_favoured_pages', 50000); + $this->rankingQueryLimit = $this->getRankingQueryLimit($this->maxRowsInTable); + } + + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + $records = []; + foreach ($this->variantByRecord() as $recordName => $variant) { + // The discrepancy_score is table-relative (see FavouredPagesScorer), so it can't be summed + // across child periods: sum the additive traffic, skip the score, then recompute it on the + // aggregated union via the transform below. Core then truncates by the recomputed score. + $records[] = Record::make(Record::TYPE_BLOB, $recordName) + ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_DISCREPANCY_SCORE) + ->setBlobColumnAggregationOps([ + Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS => 'sum', + Metrics::COLUMN_AI_CHATBOT_REQUESTS => 'sum', + Metrics::COLUMN_DISCREPANCY_SCORE => 'skip', + ]) + ->setAggregatedRecordTransform(function (DataTable $table) use ($variant): void { + (new FavouredPagesScorer($variant))->addScores($table); + }); + } + + return $records; + } + + public function isEnabled(ArchiveProcessor $archiveProcessor): bool + { + // don't process reports for any segment (these reports declare no segment support) + return $archiveProcessor->getParams()->getSegment()->isEmpty(); + } + + /** + * Day archiving: build the human/AI union from the logs once, then materialise the per-variant + * score on an independent copy for each record. Core truncates each by its score column. + */ + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + // Only the request count is needed here; the shared query's other metrics are for the Content report. + $aiTable = $this->queryPageOrDocumentUrls($archiveProcessor, Action::TYPE_PAGE_URL, $this->rankingQueryLimit, [Metrics::COLUMN_REQUESTS]); + + // With no AI chatbot page requests there is nothing to favour against, so skip the (expensive) + // human-pageviews scan and store empty records. + if ($aiTable->getRowsCount() === 0) { + Common::destroy($aiTable); + return $this->emptyRecords(); + } + + $humanTable = $this->queryHumanPageviews($archiveProcessor); + + $records = []; + foreach ($this->variantByRecord() as $recordName => $variant) { + // mergeHumanAndAiTables returns a fresh table each call, so scoring one variant cannot + // affect the other. + $table = self::mergeHumanAndAiTables($humanTable, $aiTable); + (new FavouredPagesScorer($variant))->addScores($table); + $records[$recordName] = $table; + } + + Common::destroy($humanTable); + Common::destroy($aiTable); + + return $records; + } + + /** + * @return array<string, DataTable> an empty record per variant + */ + private function emptyRecords(): array + { + $records = []; + foreach (array_keys($this->variantByRecord()) as $recordName) { + $records[$recordName] = new DataTable(); + } + + return $records; + } + + /** + * Merges the human-pageviews and AI-requests tables (both keyed on `log_action.name`) into the + * flat per-URL union: every URL on either side appears once, the missing side defaulting to 0. + * + * The two per-side truncation tails are combined into one "Others" summary row, left UNSCORED — a + * discrepancy score over an aggregate of many URLs is meaningless — so the default exclude-low- + * population filter hides it while it stays visible when that filter is off. + * + * Pure (DataTable in / out) so it is unit-testable without a database. + */ + public static function mergeHumanAndAiTables(DataTable $humanTable, DataTable $aiTable): DataTable + { + $byLabel = []; + + // The AI table comes from the shared page-metrics query, which names the request count + // COLUMN_REQUESTS (the Content report's column); read it here as this report's AI requests. + foreach ($aiTable->getRows() as $row) { + if ($row->isSummaryRow()) { + continue; + } + $label = (string) $row->getColumn('label'); + if ($label === '') { + continue; + } + $byLabel[$label] = [ + Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS => 0, + Metrics::COLUMN_AI_CHATBOT_REQUESTS => (int) $row->getColumn(Metrics::COLUMN_REQUESTS), + ]; + } + + foreach ($humanTable->getRows() as $row) { + if ($row->isSummaryRow()) { + continue; + } + $label = (string) $row->getColumn('label'); + if ($label === '') { + continue; + } + $human = (int) $row->getColumn(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS); + if (isset($byLabel[$label])) { + $byLabel[$label][Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS] = $human; + } else { + $byLabel[$label] = [ + Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS => $human, + Metrics::COLUMN_AI_CHATBOT_REQUESTS => 0, + ]; + } + } + + $table = new DataTable(); + foreach ($byLabel as $label => $cols) { + $table->addRow(new Row([ + Row::COLUMNS => [ + 'label' => (string) $label, + Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS => $cols[Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS], + Metrics::COLUMN_AI_CHATBOT_REQUESTS => $cols[Metrics::COLUMN_AI_CHATBOT_REQUESTS], + ], + ])); + } + + self::addCombinedOthersRow($table, $humanTable, $aiTable); + + return $table; + } + + /** + * Combines the human and AI tables' "Others" summary rows (the truncated tail of each side) into a + * single summary row on $table, with no discrepancy_score (see mergeHumanAndAiTables). Does nothing + * when neither side was truncated. + */ + private static function addCombinedOthersRow(DataTable $table, DataTable $humanTable, DataTable $aiTable): void + { + $humanOthers = $humanTable->getSummaryRow(); + $aiOthers = $aiTable->getSummaryRow(); + + if (!$humanOthers instanceof Row && !$aiOthers instanceof Row) { + return; + } + + $label = ($humanOthers instanceof Row ? $humanOthers : $aiOthers)->getColumn('label') ?: DataTable::LABEL_SUMMARY_ROW; + + $summary = new Row([ + Row::COLUMNS => [ + 'label' => $label, + Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS => $humanOthers instanceof Row ? (int) $humanOthers->getColumn(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS) : 0, + Metrics::COLUMN_AI_CHATBOT_REQUESTS => $aiOthers instanceof Row ? (int) $aiOthers->getColumn(Metrics::COLUMN_REQUESTS) : 0, + ], + ]); + $summary->setIsSummaryRow(); + $table->addSummaryRow($summary); + } + + /** + * @return array<string, string> record name => DiscrepancyScore variant + */ + private function variantByRecord(): array + { + return [ + Archiver::AI_CHATBOTS_HUMAN_FAVOURED_PAGES_RECORD => DiscrepancyScore::VARIANT_HUMAN_FAVOURED, + Archiver::AI_CHATBOTS_AI_FAVOURED_PAGES_RECORD => DiscrepancyScore::VARIANT_AI_FAVOURED, + ]; + } + + /** + * Human pageviews (distinct visits, matching Actions' page `nb_visits`) per page URL, keyed on + * `log_action.name` to merge directly with the AI-side counts. The `idaction_event_category IS + * NULL` clause mirrors the Actions Pages report: a URL seen only as an event context is not a + * pageview, so it must not be counted here. + */ + private function queryHumanPageviews(ArchiveProcessor $archiveProcessor): DataTable + { + $logAggregator = $archiveProcessor->getLogAggregator(); + $where = $logAggregator->getWhereStatement('log_link_visit_action', 'server_time'); + $actionTable = Common::prefixTable('log_action'); + $visitActions = Common::prefixTable('log_link_visit_action'); + + $innerSql = sprintf( + "SELECT log_action.name AS url, + COUNT(DISTINCT log_link_visit_action.idvisit) AS %s + FROM `%s` AS log_link_visit_action + INNER JOIN `%s` AS log_action ON log_action.idaction = log_link_visit_action.idaction_url + WHERE log_action.name IS NOT NULL + AND log_action.name <> '' + AND log_action.type = %d + AND log_link_visit_action.idaction_event_category IS NULL + AND %s + GROUP BY log_action.name + ORDER BY %s DESC", + Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, + $visitActions, + $actionTable, + Action::TYPE_PAGE_URL, + $where, + Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS + ); + + $rankingQuery = new RankingQuery($this->rankingQueryLimit); + $rankingQuery->addLabelColumn('url'); + $rankingQuery->addColumn(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, 'sum'); + + $wrappedSql = $rankingQuery->generateRankingQuery($innerSql); + + $stmt = Db::query($wrappedSql, $logAggregator->getGeneralQueryBindParams()); + + $table = new DataTable(); + while ($row = $stmt->fetch()) { + /** @var array<string, int|string|null> $row */ + $label = (string) $row['url']; + $raw = $row[Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS] ?? 0; + + $table->sumRowWithLabel($label, [ + Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS => is_numeric($raw) ? (int) $raw : 0, + ]); + } + + return $table; + } +} diff --git a/plugins/BotTracking/RecordBuilders/AIChatbotPageMetricsTrait.php b/plugins/BotTracking/RecordBuilders/AIChatbotPageMetricsTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..ec63cec7679e2db869b444228dc00d1d8d011f98 --- /dev/null +++ b/plugins/BotTracking/RecordBuilders/AIChatbotPageMetricsTrait.php @@ -0,0 +1,152 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\RecordBuilders; + +use InvalidArgumentException; +use Piwik\ArchiveProcessor; +use Piwik\Common; +use Piwik\Config\GeneralConfig; +use Piwik\DataTable; +use Piwik\Db; +use Piwik\Plugins\BotTracking\BotDetector; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\RankingQuery; + +/** + * Shared AI-chatbot page/document URL aggregation, used by the Content Requests + * ({@see AIChatbotContentReports}) and Favoured Pages ({@see AIChatbotFavouredPages}) builders. + * + * Keeping the per-URL request query here gives the "what counts as an AI chatbot request" predicate a + * single source of truth. Each consumer passes only the metric columns it needs, so no caller computes + * columns it discards. + */ +trait AIChatbotPageMetricsTrait +{ + /** + * The available per-URL metric columns, mapped to the SQL expression that produces them from the + * AI-chatbot request rows (the `bot` alias in {@see queryPageOrDocumentUrls}). All are summable. + * + * @return array<string, string> column name => SQL select expression + */ + protected function pageUrlMetricExpressions(): array + { + return [ + Metrics::COLUMN_REQUESTS => 'COUNT(*)', + Metrics::COLUMN_SUM_SERVER_TIME => 'SUM(bot.response_time_ms)', + Metrics::COLUMN_NB_SERVER_TIME => 'SUM(CASE WHEN bot.response_time_ms IS NOT NULL THEN 1 ELSE 0 END)', + Metrics::COLUMN_SUM_RESPONSE_SIZE => 'SUM(bot.response_size_bytes)', + Metrics::COLUMN_NB_RESPONSE_SIZE => 'SUM(CASE WHEN bot.response_size_bytes IS NOT NULL THEN 1 ELSE 0 END)', + Metrics::COLUMN_PAGE_NOT_FOUND_404_REQUESTS => 'SUM(bot.http_status_code IN (404, 410))', + Metrics::COLUMN_SERVER_ERROR_5XX_REQUESTS => 'SUM(bot.http_status_code BETWEEN 500 AND 599)', + ]; + } + + /** + * Queries page or document URLs requested by AI chatbots, selecting only the requested metric + * columns (Content the full set, Favoured Pages just the request count). + * + * @param string[] $columnNames metric columns to select, from {@see pageUrlMetricExpressions} keys + */ + protected function queryPageOrDocumentUrls(ArchiveProcessor $archiveProcessor, int $actionType, int $rankingQueryLimit, array $columnNames): DataTable + { + $logAggregator = $archiveProcessor->getLogAggregator(); + $where = $logAggregator->getWhereStatement('bot', 'server_time'); + $botTable = BotRequestsDao::getPrefixedTableName(); + $actionTable = Common::prefixTable('log_action'); + + $expressions = $this->pageUrlMetricExpressions(); + $selects = ['log_action.name AS url']; + $columns = []; + foreach ($columnNames as $columnName) { + if (!isset($expressions[$columnName])) { + throw new InvalidArgumentException('Unknown AI chatbot page metric column: ' . $columnName); + } + $selects[$columnName] = $expressions[$columnName] . ' AS ' . $columnName; + $columns[$columnName] = 'sum'; + } + + // Truncate by the request count when present (every consumer selects it); otherwise the first + // requested column, so the RankingQuery keeps the highest-volume rows. + $orderColumn = isset($columns[Metrics::COLUMN_REQUESTS]) ? Metrics::COLUMN_REQUESTS : (string) array_key_first($columns); + + $innerSql = sprintf( + "SELECT %s + FROM `%s` AS bot + INNER JOIN `%s` AS log_action ON log_action.idaction = bot.idaction_url + WHERE log_action.name IS NOT NULL + AND log_action.name <> '' + AND log_action.type = %d + AND bot.bot_type = ? + AND %s + GROUP BY log_action.name + ORDER BY %s DESC", + implode(",\n ", $selects), + $botTable, + $actionTable, + $actionType, + $where, + $orderColumn + ); + + return $this->executeUrlQuery($archiveProcessor, $innerSql, $columns, $rankingQueryLimit); + } + + /** + * Wraps an already-built inner SQL with a RankingQuery and populates a DataTable; the "Others" + * summary row is routed by sumRowWithLabel. + * + * @param string $innerSql Already-interpolated SQL (bot_type bind placeholder kept as `?`). + * @param array<string, string> $columns column name → RankingQuery aggregation op; must match the + * SELECT aliases. + */ + protected function executeUrlQuery(ArchiveProcessor $archiveProcessor, string $innerSql, array $columns, int $rankingQueryLimit): DataTable + { + $logAggregator = $archiveProcessor->getLogAggregator(); + + $rankingQuery = new RankingQuery($rankingQueryLimit); + $rankingQuery->addLabelColumn('url'); + + foreach ($columns as $column => $op) { + $rankingQuery->addColumn($column, $op); + } + + $wrappedSql = $rankingQuery->generateRankingQuery($innerSql); + + $bind = array_merge([BotDetector::BOT_TYPE_AI_CHATBOT], $logAggregator->getGeneralQueryBindParams()); + $stmt = Db::query($wrappedSql, $bind); + + $table = new DataTable(); + while ($row = $stmt->fetch()) { + /** @var array<string, int|string|null> $row */ + $label = (string) $row['url']; + + $metrics = []; + foreach ($columns as $column => $op) { + $raw = $row[$column] ?? 0; + $metrics[$column] = is_numeric($raw) ? (int) $raw : 0; + } + + $table->sumRowWithLabel($label, $metrics); + } + + return $table; + } + + protected function getRankingQueryLimit(int $maxRowsInTable): int + { + $configLimit = GeneralConfig::getIntegerConfigValue('archiving_ranking_query_row_limit', 0); + + // As we are querying flat data, use `maxRowsInTable` as ranking query limit as it would be pointless to query more + return max($configLimit, $maxRowsInTable); + } +} diff --git a/plugins/BotTracking/RecordBuilders/AIChatbotReports.php b/plugins/BotTracking/RecordBuilders/AIChatbotReports.php new file mode 100644 index 0000000000000000000000000000000000000000..2066a763692cbe3778ce518be33ea5b7023c87ab --- /dev/null +++ b/plugins/BotTracking/RecordBuilders/AIChatbotReports.php @@ -0,0 +1,318 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\RecordBuilders; + +use Piwik\ArchiveProcessor; +use Piwik\ArchiveProcessor\Record; +use Piwik\ArchiveProcessor\RecordBuilder; +use Piwik\Common; +use Piwik\Config\GeneralConfig; +use Piwik\DataAccess\LogAggregator; +use Piwik\DataTable; +use Piwik\Db; +use Piwik\Plugins\BotTracking\Archiver; +use Piwik\Plugins\BotTracking\BotDetector; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\RankingQuery; +use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; + +class AIChatbotReports extends RecordBuilder +{ + /** + * @var array<string, string> + */ + public const CHATBOT_MAPPING = [ + 'ChatGPT-User' => 'ChatGPT', + 'MistralAI-User' => 'Le Chat', + 'Gemini-Deep-Research' => 'Gemini', + 'Claude-User' => 'Claude', + 'Perplexity-User' => 'Perplexity', + 'Google-NotebookLM' => 'NotebookLM', + ]; + + /** + * @var int + */ + private $rankingQueryLimit; + + public function __construct() + { + parent::__construct(); + + $this->maxRowsInTable = GeneralConfig::getIntegerConfigValue('datatable_archiving_maximum_rows_bots', 0); + $this->maxRowsInSubtable = GeneralConfig::getIntegerConfigValue('datatable_archiving_maximum_rows_subtable_bots', 0); + $this->rankingQueryLimit = $this->getRankingQueryLimit(); + $this->columnAggregationOps = [ + Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS => 'skip', + Metrics::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS => 'skip', + ]; + } + + public function getRecordMetadata(ArchiveProcessor $archiveProcessor): array + { + return [ + Record::make(Record::TYPE_BLOB, Archiver::AI_CHATBOTS_PAGES_RECORD) + ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_REQUESTS), + Record::make(Record::TYPE_BLOB, Archiver::AI_CHATBOTS_DOCUMENTS_RECORD) + ->setColumnToSortByBeforeTruncation(Metrics::COLUMN_REQUESTS), + Record::make(Record::TYPE_NUMERIC, Metrics::METRIC_AI_CHATBOTS_UNIQUE_CHATBOTS) + ->setIsCountOfBlobRecordRows(Archiver::AI_CHATBOTS_PAGES_RECORD), + Record::make(Record::TYPE_NUMERIC, Metrics::METRIC_AI_CHATBOTS_REQUESTS), + Record::make(Record::TYPE_NUMERIC, Metrics::METRIC_AI_CHATBOTS_ACQUIRED_VISITS), + Record::make(Record::TYPE_NUMERIC, Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS), + Record::make(Record::TYPE_NUMERIC, Metrics::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS), + Record::make(Record::TYPE_NUMERIC, Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS), + Record::make(Record::TYPE_NUMERIC, Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS), + ]; + } + + public function isEnabled(ArchiveProcessor $archiveProcessor): bool + { + // don't process reports for any segment + return $archiveProcessor->getParams()->getSegment()->isEmpty(); + } + + protected function aggregate(ArchiveProcessor $archiveProcessor): array + { + $tables = [ + Archiver::AI_CHATBOTS_PAGES_RECORD => new DataTable(), + Archiver::AI_CHATBOTS_DOCUMENTS_RECORD => new DataTable(), + ]; + + $this->populateTables($archiveProcessor, $tables); + $this->populateNumerics($archiveProcessor, $tables); + + return $tables; + } + + /** + * @param array<string, DataTable> $tables + */ + private function populateTables(ArchiveProcessor $archiveProcessor, array &$tables): void + { + $logAggregator = $archiveProcessor->getLogAggregator(); + $visits = $this->queryAcquiredVisitsByAIChatbot($logAggregator); + + $this->populateChatbotTableForActionType($tables, Action::TYPE_PAGE_URL, $logAggregator, $visits); + $this->populateChatbotTableForActionType($tables, Action::TYPE_DOWNLOAD, $logAggregator, $visits); + } + + /** + * @return array<string,int> + */ + private function queryAcquiredVisitsByAIChatbot(LogAggregator $logAggregator): array + { + $where = $logAggregator->getWhereStatement('log_visit', 'visit_last_action_time'); + $bindBase = $logAggregator->getGeneralQueryBindParams(); + + $sql = sprintf( + "SELECT `referer_name`, COUNT(*) AS `visits` + FROM %s AS `log_visit` + WHERE `referer_type` = %d + AND `referer_name` <> '' + AND %s + GROUP BY `referer_name`", + Common::prefixTable('log_visit'), + Common::REFERRER_TYPE_AI_ASSISTANT, + $where + ); + + $stmt = Db::query($sql, $bindBase); + $result = []; + + while ($row = $stmt->fetch()) { + /** + * @var array{visits: string|int, referer_name: string} $row + */ + if (in_array($row['referer_name'], self::CHATBOT_MAPPING)) { + $key = (string)array_search($row['referer_name'], self::CHATBOT_MAPPING); + $result[$key] = (int)$row['visits']; + } + } + + return $result; + } + + /** + * @param array<string, DataTable> $tables + * @param array<string, int> $visits + */ + private function populateChatbotTableForActionType(array $tables, int $actionType, LogAggregator $logAggregator, array $visits): void + { + $resultSet = $this->queryBotRequests($logAggregator, $actionType); + + while ($row = $resultSet->fetch()) { + /** + * @var array{requests: int, bot_name: ?string, url: ?string} $row + */ + $label = $row['bot_name']; + $url = $row['url']; + + if ($label === null) { + // top-level rollup result + continue; + } + + if ($url === null) { + // second-level rollup result + $metrics = [ + Metrics::COLUMN_REQUESTS => $row['requests'], + Metrics::COLUMN_DOCUMENT_REQUESTS => $actionType === Action::TYPE_DOWNLOAD ? $row['requests'] : 0, + Metrics::COLUMN_PAGE_REQUESTS => $actionType === Action::TYPE_PAGE_URL ? $row['requests'] : 0, + Metrics::COLUMN_ACQUIRED_VISITS => $visits[$label] ?? 0, + ]; + + $tables[Archiver::AI_CHATBOTS_PAGES_RECORD]->sumRowWithLabel($label, $metrics, [Metrics::COLUMN_ACQUIRED_VISITS => 'max']); + $tables[Archiver::AI_CHATBOTS_DOCUMENTS_RECORD]->sumRowWithLabel($label, $metrics, [Metrics::COLUMN_ACQUIRED_VISITS => 'max']); + continue; + } + + + $table = $tables[Archiver::AI_CHATBOTS_PAGES_RECORD]; + + if ($actionType === Action::TYPE_DOWNLOAD) { + $table = $tables[Archiver::AI_CHATBOTS_DOCUMENTS_RECORD]; + } + + $tableRow = $table->getRowFromLabel($label); + + if (false === $tableRow) { + // non-rollup row but rollup row is missing + // should not happen, but don't break + continue; + } + + if ( + $url === RankingQuery::LABEL_SUMMARY_ROW + && !$tableRow->isSubtableLoaded() + ) { + // skip creating the subtable if: + // - we are using rollups + // - the only row would be "Others" + continue; + } + + $normalized = PageUrl::normalizeUrl($url); + $url = $normalized['url']; + + $tableRow->sumRowWithLabelToSubtable($url, [ + Metrics::COLUMN_REQUESTS => $row['requests'], + ]); + } + } + + private function queryBotRequests(LogAggregator $logAggregator, int $actionType) + { + $where = $logAggregator->getWhereStatement('bot', 'server_time'); + + $sql = sprintf( + "SELECT * FROM (SELECT bot.bot_name, log_action.name AS url, COUNT(*) AS requests + FROM `%s` AS bot + INNER JOIN `%s` AS log_action ON log_action.idaction = bot.idaction_url + WHERE log_action.name IS NOT NULL + AND log_action.name <> '' + AND log_action.type = %d + AND bot.bot_type = ? + AND %s + GROUP BY bot.bot_name, url WITH ROLLUP) AS rollupQuery + ORDER BY requests DESC, bot_name, url", + BotRequestsDao::getPrefixedTableName(), + Common::prefixTable('log_action'), + $actionType, + $where + ); + + if ($this->rankingQueryLimit > 0) { + $rankingQuery = new RankingQuery($this->rankingQueryLimit); + $rankingQuery->addLabelColumn(['bot_name', 'url']); + $rankingQuery->addColumn('requests', 'sum'); + $sql = $rankingQuery->generateRankingQuery($sql, true); + } + + return Db::query($sql, array_merge([BotDetector::BOT_TYPE_AI_CHATBOT], $logAggregator->getGeneralQueryBindParams())); + } + + private function getRankingQueryLimit(): int + { + $maxRowsInTable = (int)$this->maxRowsInTable; + $maxRowsInSubtable = (int)$this->maxRowsInSubtable; + + $configLimit = GeneralConfig::getIntegerConfigValue('archiving_ranking_query_row_limit', 0); + $configLimit = max($configLimit, 10 * $maxRowsInTable); + + if ($configLimit === 0) { + return 0; + } + + return max($configLimit, $maxRowsInTable, $maxRowsInSubtable); + } + + /** + * @param array<string, DataTable> $tables + */ + private function populateNumerics(ArchiveProcessor $archiveProcessor, array &$tables): void + { + $logAggregator = $archiveProcessor->getLogAggregator(); + + $table = BotRequestsDao::getPrefixedTableName(); + $visitTable = Common::prefixTable('log_visit'); + $actionTable = Common::prefixTable('log_action'); + + $where = $logAggregator->getWhereStatement('bot', 'server_time'); + + $sql = <<<SQL +SELECT + COUNT(*) AS requests, + SUM(CASE WHEN bot.http_status_code IN (404, 410) THEN 1 ELSE 0 END) AS not_found_requests, + SUM(CASE WHEN bot.http_status_code BETWEEN 500 AND 599 THEN 1 ELSE 0 END) AS server_error_requests, + COUNT(DISTINCT bot.bot_name) AS uniq_bots, + COUNT(DISTINCT(CASE WHEN log_action.type = ? THEN log_action.name END)) AS uniq_pages, + COUNT(DISTINCT(CASE WHEN log_action.type = ? THEN log_action.name END)) AS uniq_downloads +FROM `$table` AS bot +LEFT JOIN `$actionTable` AS log_action ON log_action.idaction = bot.idaction_url +WHERE bot.bot_type = ? AND $where +SQL; + + $bind = [ + Action::TYPE_PAGE_URL, + Action::TYPE_DOWNLOAD, + BotDetector::BOT_TYPE_AI_CHATBOT, + ]; + $bind = array_merge($bind, $logAggregator->getGeneralQueryBindParams()); + + $row = Db::fetchRow($sql, $bind) ?: []; + + $visitBind = [ + Common::REFERRER_TYPE_AI_ASSISTANT, + ]; + $visitBind = array_merge($visitBind, $logAggregator->getGeneralQueryBindParams()); + + $where = $logAggregator->getWhereStatement('log_visit', 'visit_last_action_time'); + + $visitsSql = sprintf( + "SELECT COUNT(*) FROM `%s` log_visit WHERE referer_type = ? AND $where", + $visitTable + ); + + $acquiredVisits = (int)Db::fetchOne($visitsSql, $visitBind); + + $tables[Metrics::METRIC_AI_CHATBOTS_UNIQUE_CHATBOTS] = (int)($row['uniq_bots'] ?? 0); + $tables[Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS] = (int)($row['uniq_pages'] ?? 0); + $tables[Metrics::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS] = (int)($row['uniq_downloads'] ?? 0); + $tables[Metrics::METRIC_AI_CHATBOTS_REQUESTS] = (int)($row['requests'] ?? 0); + $tables[Metrics::METRIC_AI_CHATBOTS_ACQUIRED_VISITS] = $acquiredVisits; + $tables[Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS] = (int)($row['not_found_requests'] ?? 0); + $tables[Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS] = (int)($row['server_error_requests'] ?? 0); + } +} diff --git a/plugins/BotTracking/Reports/AIChatbotsRealTimeWidgets.php b/plugins/BotTracking/Reports/AIChatbotsRealTimeWidgets.php new file mode 100644 index 0000000000000000000000000000000000000000..15f4b843653ef025ce8306fc021dffc9f7da92b6 --- /dev/null +++ b/plugins/BotTracking/Reports/AIChatbotsRealTimeWidgets.php @@ -0,0 +1,68 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +final class AIChatbotsRealTimeWidgets +{ + public const LOOKBACK_30_MINUTES = 30; + public const LOOKBACK_480_MINUTES = 480; // 8 hours + + /** + * @return array<int, array{name: string, documentation: string, lastMinutes: int, order: int}> + */ + public static function getChatbotWidgets(): array + { + return [ + [ + 'name' => 'BotTracking_AIChatbotsLast30MinutesTitle', + 'documentation' => 'BotTracking_AIChatbotsLast30MinutesDocumentation', + 'lastMinutes' => self::LOOKBACK_30_MINUTES, + 'order' => 10, + ], + [ + 'name' => 'BotTracking_AIChatbotsLast8HoursTitle', + 'documentation' => 'BotTracking_AIChatbotsLast8HoursDocumentation', + 'lastMinutes' => self::LOOKBACK_480_MINUTES, + 'order' => 20, + ], + ]; + } + + /** + * @return array<int, array{name: string, documentation: string, lastMinutes: int, order: int}> + */ + public static function getTopPageUrlWidgets(): array + { + return [ + [ + 'name' => 'BotTracking_TopPageUrlsLast30MinutesTitle', + 'documentation' => 'BotTracking_TopPageUrlsLast30MinutesDocumentation', + 'lastMinutes' => self::LOOKBACK_30_MINUTES, + 'order' => 30, + ], + [ + 'name' => 'BotTracking_TopPageUrlsLast8HoursTitle', + 'documentation' => 'BotTracking_TopPageUrlsLast8HoursDocumentation', + 'lastMinutes' => self::LOOKBACK_480_MINUTES, + 'order' => 40, + ], + ]; + } + + /** + * @return array<int, array{name: string, documentation: string, lastMinutes: int, order: int}> + */ + public static function getAllWidgets(): array + { + return array_merge(self::getChatbotWidgets(), self::getTopPageUrlWidgets()); + } +} diff --git a/plugins/BotTracking/Reports/AbstractAIChatbotContentUrlReport.php b/plugins/BotTracking/Reports/AbstractAIChatbotContentUrlReport.php new file mode 100644 index 0000000000000000000000000000000000000000..6f06b2dff6107c9f2f35444e4c088f6c6a516011 --- /dev/null +++ b/plugins/BotTracking/Reports/AbstractAIChatbotContentUrlReport.php @@ -0,0 +1,112 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\DataTable; +use Piwik\Piwik; +use Piwik\Plugin\Report; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\BotTracking\Columns\Metrics\AvgResponseSize; +use Piwik\Plugins\BotTracking\Columns\Metrics\AvgServerTime; +use Piwik\Plugins\BotTracking\Columns\Metrics\PageNotFound404Requests; +use Piwik\Plugins\BotTracking\Columns\Metrics\Requests; +use Piwik\Plugins\BotTracking\Columns\Metrics\ServerError5xxRequests; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Report\ReportWidgetFactory; +use Piwik\Widget\WidgetsList; + +/** + * Base class for the Pages and Documents content-URL reports. + * + * Owns the shared metric set, configureView() defaults, configureWidgets(), and the per-report + * scoped Requests documentation override. The error-status columns (page_not_found_404_requests, + * server_error_5xx_requests) are registered in $this->metrics so they remain available via the + * API, Custom Alerts, Scheduled Reports and Row Evolution — but the "show all columns" table + * toggle is disabled because that view uses the Visitor Engagement preset which doesn't match + * the BotTracking column schema (and would render an empty table). + * + * GetAIChatbotBrokenContent stays as a direct Report subclass — it has a different metric shape. + */ +abstract class AbstractAIChatbotContentUrlReport extends Report +{ + protected function init(): void + { + parent::init(); + + $this->categoryId = 'General_AIAssistants'; + $this->subcategoryId = 'BotTracking_AIChatbotsContentRequests'; + $this->metrics = [new Requests(), new ServerError5xxRequests(), new PageNotFound404Requests()]; + $this->processedMetrics = [new AvgServerTime(), new AvgResponseSize()]; + $this->defaultSortColumn = Metrics::COLUMN_REQUESTS; + } + + public function configureView(ViewDataTable $view): void + { + parent::configureView($view); + + $view->config->setDefaultColumnsToDisplay( + ['label', Metrics::COLUMN_REQUESTS, Metrics::COLUMN_AVG_SERVER_TIME, Metrics::COLUMN_AVG_RESPONSE_SIZE], + false, + false + ); + + // Show-all-columns switches to the Visitor Engagement preset, which doesn't fit this schema. + $view->config->show_table_all_columns = false; + + // Insights and bar/pie/tag-cloud all assume visit metrics (nb_visits etc.) this report lacks, + // so they would render empty — the table is the only useful view. + $view->config->show_insights = false; + $view->config->show_bar_chart = false; + $view->config->show_pie_chart = false; + $view->config->show_tag_cloud = false; + + // Render URL labels as clickable links (scheme-less normalized URLs; prepend https://). + $view->config->filters[] = function (DataTable $table) { + foreach ($table->getRows() as $row) { + if ($row->isSummaryRow()) { + continue; + } + $label = $row->getColumn('label'); + if (is_string($label) && $label !== '') { + $row->setMetadata('url', 'https://' . $label); + } + } + }; + + SegmentNotSupportedMessageHelper::addSegmentNotSupportedMessage($view); + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory): void + { + $widgetsList->addWidgetConfig($factory->createWidget()->setIsWide()); + } + + /** + * Returns the translation key for the scoped "Requests" tooltip for this report. + * Concrete subclasses return the key that scopes the tooltip to their action type + * (page URLs or document URLs). + */ + abstract protected function getRequestsDocumentationKey(): string; + + /** + * @return array<string, string> + */ + public function getMetricsDocumentation(): array + { + $docs = parent::getMetricsDocumentation(); + + // Scope the "Requests" tooltip to this report's action type (page URLs or document URLs). + $docs[Metrics::COLUMN_REQUESTS] = Piwik::translate($this->getRequestsDocumentationKey()); + + return $docs; + } +} diff --git a/plugins/BotTracking/Reports/AbstractAIChatbotFavouredPagesReport.php b/plugins/BotTracking/Reports/AbstractAIChatbotFavouredPagesReport.php new file mode 100644 index 0000000000000000000000000000000000000000..33a28330389efc9f3afad5221e9d6c9a45a58f35 --- /dev/null +++ b/plugins/BotTracking/Reports/AbstractAIChatbotFavouredPagesReport.php @@ -0,0 +1,209 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Common; +use Piwik\DataTable; +use Piwik\Plugin\Report; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\Actions\Columns\PageUrl; +use Piwik\Plugins\BotTracking\Columns\Metrics\AIChatbotRequests; +use Piwik\Plugins\BotTracking\Columns\Metrics\DiscrepancyScore; +use Piwik\Plugins\BotTracking\Columns\Metrics\UniqueHumanPageviews; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Report\ReportWidgetFactory; +use Piwik\Widget\WidgetsList; + +/** + * Shared base for the Human-Favoured and AI-Favoured Pages reports. + * + * Both reports expose the same flat URL dimension, the same Unique Human Pageviews + + * AI Chatbot Requests metric pair, and a Discrepancy Score whose variant (human-favoured vs + * ai-favoured) is provided by the concrete subclass. Each variant is backed by its own archived + * blob record built and scored during archiving (see {@see \Piwik\Plugins\BotTracking\RecordBuilders\AIChatbotFavouredPages}); + * the API just reads it. Report-level surfaces (Custom Alerts, Scheduled Reports, glossary) treat + * them as ordinary reports, and Row Evolution is supported because the per-period data is pre-computed. + */ +abstract class AbstractAIChatbotFavouredPagesReport extends Report +{ + protected function init(): void + { + parent::init(); + + $this->categoryId = 'General_AIAssistants'; + $this->subcategoryId = 'BotTracking_AIChatbotsContentRequests'; + $this->dimension = new PageUrl(); + // discrepancy_score is materialised during archiving (see AIChatbotFavouredPages / + // FavouredPagesScorer) and read straight back, so it is an ordinary column here rather than a + // recomputed processed metric. The two traffic metrics are ordered strong-side first (the + // column the report favours leads), so AI-Favoured leads with AI Chatbot Requests. + $human = new UniqueHumanPageviews(); + $ai = new AIChatbotRequests(); + $trafficMetrics = $this->getDiscrepancyScoreVariant() === DiscrepancyScore::VARIANT_AI_FAVOURED + ? [$ai, $human] + : [$human, $ai]; + $this->metrics = array_merge( + $trafficMetrics, + [new DiscrepancyScore($this->getDiscrepancyScoreVariant())] + ); + // No processed metrics; don't inherit Report's core visitor defaults. + $this->processedMetrics = []; + // Both reports sort by the Discrepancy Score — that's the headline insight, and it already + // encodes traffic weighting, so sorting by it surfaces the genuinely (human/AI)-favoured + // pages rather than just the busiest ones. + $this->defaultSortColumn = Metrics::COLUMN_DISCREPANCY_SCORE; + } + + /** + * @return DiscrepancyScore::VARIANT_HUMAN_FAVOURED|DiscrepancyScore::VARIANT_AI_FAVOURED + */ + abstract protected function getDiscrepancyScoreVariant(): string; + + /** + * The report's strong-side traffic column (human pageviews for Human-Favoured, AI requests for + * AI-Favoured). + */ + private function getStrongSideColumn(): string + { + return $this->getDiscrepancyScoreVariant() === DiscrepancyScore::VARIANT_HUMAN_FAVOURED + ? Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS + : Metrics::COLUMN_AI_CHATBOT_REQUESTS; + } + + /** + * The two traffic columns ordered strong-side first, so each report leads with the metric it + * favours (AI Chatbot Requests for AI-Favoured, Unique Human Pageviews for Human-Favoured). + * + * @return string[] + */ + private function getTrafficColumnsInDisplayOrder(): array + { + return $this->getDiscrepancyScoreVariant() === DiscrepancyScore::VARIANT_AI_FAVOURED + ? [Metrics::COLUMN_AI_CHATBOT_REQUESTS, Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS] + : [Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, Metrics::COLUMN_AI_CHATBOT_REQUESTS]; + } + + /** + * Breaks ties on the Discrepancy Score by the report's strong-side traffic, so pages with the + * same score (notably the many score=0 rows when the low-population filter is off) are ordered + * by how much human / AI traffic they have instead of arbitrarily. If the user sorts by a + * different column, the score becomes the tie-breaker. + */ + public function getSecondarySortColumnCallback() + { + $strongColumn = $this->getStrongSideColumn(); + + return function ($primaryColumn) use ($strongColumn) { + return $primaryColumn === Metrics::COLUMN_DISCREPANCY_SCORE + ? $strongColumn + : Metrics::COLUMN_DISCREPANCY_SCORE; + }; + } + + public function configureView(ViewDataTable $view): void + { + parent::configureView($view); + + $view->config->setDefaultColumnsToDisplay( + array_merge( + ['label'], + $this->getTrafficColumnsInDisplayOrder(), + [Metrics::COLUMN_DISCREPANCY_SCORE] + ), + false, + false + ); + + // Show-all-columns switches to the Visitor Engagement preset, which doesn't fit this schema. + $view->config->show_table_all_columns = false; + + // Insights and bar/pie/tag-cloud all assume visit metrics (nb_visits etc.) this report lacks, + // so they would render empty — the table is the only useful view. + $view->config->show_insights = false; + $view->config->show_bar_chart = false; + $view->config->show_pie_chart = false; + $view->config->show_tag_cloud = false; + + // Render URL labels as clickable links (scheme-less normalized URLs; prepend https://). + $view->config->filters[] = function (DataTable $table) { + foreach ($table->getRows() as $row) { + if ($row->isSummaryRow()) { + continue; + } + $label = $row->getColumn('label'); + if (is_string($label) && $label !== '') { + $row->setMetadata('url', 'https://' . $label); + } + } + }; + + // Force the default sort at the View layer too: RequestConfig::setDefaultSort ignores the + // Report's $defaultSortColumn — it falls back to nb_visits (absent here) and then to the + // first non-label column in columns_to_display. Set it explicitly so the Discrepancy Score + // is the default sort in the UI as well as via the API. + $view->requestConfig->filter_sort_column = $this->defaultSortColumn; + $view->requestConfig->filter_sort_order = 'desc'; + + $this->configureExcludeLowPopulation($view); + + SegmentNotSupportedMessageHelper::addSegmentNotSupportedMessage($view); + } + + /** + * Excludes pages that aren't meaningfully favoured by dropping rows whose Discrepancy Score is + * below 1. Because the score is `lean × volume`, this removes balanced and opposite-leaning + * pages (lean = 0 → score 0) as well as the near-zero-volume tail, so each report shows only + * pages leaning its own way. The toggle defaults to ON; `enable_filter_excludelowpop=0` shows + * every row. + * + * Wired through the standard ExcludeLowPopulation generic filter targeting the score column. + * The score is a stored column (materialised during archiving), so it is present on every row by + * the time the filter runs. The minimum value must stay > 0 — passing 0 makes ExcludeLowPopulation + * fall back to its 2%-of-sum heuristic and empty the table. + */ + private function configureExcludeLowPopulation(ViewDataTable $view): void + { + $view->config->show_exclude_low_population = true; + + $enabled = Common::getRequestVar('enable_filter_excludelowpop', '1', 'string') !== '0'; + + // Surface the resolved toggle state to the client so the data-table footer label + // ("Include/Exclude Rows With Low Population") matches whether the filter is actually + // active on initial load, without the user having to interact with it first. + $view->config->custom_parameters['enable_filter_excludelowpop'] = $enabled ? '1' : '0'; + + if (!$enabled) { + return; + } + + $view->requestConfig->filter_excludelowpop = Metrics::COLUMN_DISCREPANCY_SCORE; + $view->requestConfig->filter_excludelowpop_value = '1'; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory): void + { + // Side-by-side layout contract: the Human-Favoured and AI-Favoured reports must + // render next to each other in a 2-column row on the AIChatbotsContentRequests page. Matomo + // has no 2-equal-column widget-container primitive, so this relies on the reporting page + // auto-pairing CONSECUTIVE NON-WIDE widgets into columns (see CoreHome + // ReportingPage.store::widgets). On this page the non-wide widgets are Documents, Broken, + // Human-Favoured and AI-Favoured (orders 20/30/40/50); the auto-pairing distributes them + // across two columns so Documents sits beside Broken and Human-Favoured beside AI-Favoured. + // For that to hold, all of the following must stay true (the BotTracking_spec.js pairing + // assertions are the load-bearing guard): + // - none of those four reports call setIsWide() (hence the bare createWidget() below); + // - they keep orders 40 and 50 so they stay paired with each other (after Documents/Broken); + // - the Pages report (order 10) stays wide so it remains a full-width row on top; + // - no other plugin injects a non-wide widget between/around them in this subcategory. + $widgetsList->addWidgetConfig($factory->createWidget()); + } +} diff --git a/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeChatbotsReport.php b/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeChatbotsReport.php new file mode 100644 index 0000000000000000000000000000000000000000..d0cf6a3a3c4d99a6713209fb34852c78704b6de8 --- /dev/null +++ b/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeChatbotsReport.php @@ -0,0 +1,56 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\BotTracking\Columns\AIChatbotName; +use Piwik\Plugins\BotTracking\Columns\Metrics\Requests; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; +use Piwik\Plugins\BotTracking\Metrics; + +abstract class AbstractAIChatbotsRealTimeChatbotsReport extends AbstractAIChatbotsRealTimeReport +{ + protected function init(): void + { + parent::init(); + + $this->dimension = new AIChatbotName(); + $this->metrics = [ + new Requests(), + Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS, + Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS, + Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS, + ]; + } + + public function configureView(ViewDataTable $view): void + { + parent::configureView($view); + + $view->config->setDefaultColumnsToDisplay( + [ + 'label', + Metrics::COLUMN_REQUESTS, + Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS, + Metrics::METRIC_AI_CHATBOTS_NOT_FOUND_REQUESTS, + Metrics::METRIC_AI_CHATBOTS_SERVER_ERROR_REQUESTS, + ], + false, + false + ); + } + + protected function getReportRowLimit(): int + { + return BotRequestsDao::getAIChatbotActivityForDateRangeLimit(); + } +} diff --git a/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeReport.php b/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeReport.php new file mode 100644 index 0000000000000000000000000000000000000000..17688925c1bc5aa708ed8a5d580bb8a1e299e689 --- /dev/null +++ b/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeReport.php @@ -0,0 +1,81 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Plugin\Report; +use Piwik\Plugin\ViewDataTable; +use Piwik\Piwik; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Report\ReportWidgetFactory; +use Piwik\Widget\WidgetsList; + +abstract class AbstractAIChatbotsRealTimeReport extends Report +{ + protected function init(): void + { + parent::init(); + + $this->categoryId = 'General_AIAssistants'; + $this->subcategoryId = 'BotTracking_AIChatbotsRealtime'; + $this->processedMetrics = []; + $this->defaultSortColumn = Metrics::COLUMN_REQUESTS; + } + + public function configureReportMetadata(&$availableReports, $infos) + { + return false; + } + + public function configureView(ViewDataTable $view): void + { + parent::configureView($view); + + $view->requestConfig->filter_sort_column = $this->defaultSortColumn; + $view->requestConfig->filter_sort_order = 'desc'; + $view->config->show_footer_message = Piwik::translate( + 'BotTracking_AIChatbotsRealTimeReportLimitFooter', + [$this->getReportRowLimit()] + ); + + $view->config->metrics_documentation = array_merge( + $view->config->metrics_documentation, + $this->getMetricsDocumentation() + ); + + $view->config->show_table_all_columns = false; + $view->config->show_insights = false; + $view->config->show_bar_chart = false; + $view->config->show_pie_chart = false; + $view->config->show_tag_cloud = false; + $view->config->disable_row_evolution = true; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory): void + { + foreach ($this->getWidgetDefinitions() as $definition) { + $widget = $factory->createWidget() + ->setName($definition['name']) + ->setOrder($definition['order']) + ->setParameters(['lastMinutes' => $definition['lastMinutes']]) + ->setIsWide(); + + $widgetsList->addWidgetConfig($widget); + } + } + + /** + * @return array<int, array{name: string, documentation: string, lastMinutes: int, order: int}> + */ + abstract protected function getWidgetDefinitions(): array; + + abstract protected function getReportRowLimit(): int; +} diff --git a/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeTopPageUrlsReport.php b/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeTopPageUrlsReport.php new file mode 100644 index 0000000000000000000000000000000000000000..46d606510387a7956fec1bfb52a3805c44568664 --- /dev/null +++ b/plugins/BotTracking/Reports/AbstractAIChatbotsRealTimeTopPageUrlsReport.php @@ -0,0 +1,47 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\BotTracking\Columns\Metrics\Requests; +use Piwik\Plugins\BotTracking\Columns\PageUrl; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; +use Piwik\Plugins\BotTracking\Metrics; + +abstract class AbstractAIChatbotsRealTimeTopPageUrlsReport extends AbstractAIChatbotsRealTimeReport +{ + protected function init(): void + { + parent::init(); + + $this->dimension = new PageUrl(); + $this->metrics = [ + new Requests(), + ]; + } + + public function configureView(ViewDataTable $view): void + { + parent::configureView($view); + + $view->config->setDefaultColumnsToDisplay( + ['label', Metrics::COLUMN_REQUESTS], + false, + false + ); + } + + protected function getReportRowLimit(): int + { + return BotRequestsDao::getAIChatbotTopPageUrlsForDateRangeLimit(); + } +} diff --git a/plugins/BotTracking/Reports/Get.php b/plugins/BotTracking/Reports/Get.php new file mode 100644 index 0000000000000000000000000000000000000000..01495fb0af0ae2126a759aefab134bf7fdeca5a8 --- /dev/null +++ b/plugins/BotTracking/Reports/Get.php @@ -0,0 +1,88 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\Report; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\BotTracking\Columns\Metrics\ClickThroughRate; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution; +use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines; +use Piwik\Report\ReportWidgetFactory; +use Piwik\Widget\WidgetsList; + +class Get extends Report +{ + protected function init(): void + { + parent::init(); + $this->categoryId = 'General_AIAssistants'; + $this->subcategoryId = 'BotTracking_AIChatbotsOverview'; + $this->name = Piwik::translate('BotTracking_ReportTitleChatbotsOverview'); + $this->documentation = ''; + $this->metrics = Metrics::getReportMetricColumns(); + $this->processedMetrics = [ + new ClickThroughRate(), + ]; + $this->order = 10; + + if (\Piwik\Request::fromRequest()->getStringParameter('period', '') !== 'day') { + $this->metrics = array_filter($this->metrics, function ($metric) { + return !in_array($metric, [Metrics::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS, Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS]); + }); + } + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory): void + { + $widgetsList->addWidgetConfig( + $factory->createWidget() + ->setName('BotTracking_ReportTitleChatbotsOverTime') + ->forceViewDataTable(Evolution::ID) + ->setAction('getEvolutionGraph') + ->setOrder(1) + ); + + $widgetsList->addWidgetConfig( + $factory->createWidget() + ->setName('BotTracking_ReportTitleChatbotsOverview') + ->forceViewDataTable(Sparklines::ID) + ->setOrder(2) + ); + } + + public function configureView(ViewDataTable $view): void + { + if (!$view->isViewDataTableId(Sparklines::ID)) { + return; + } + + /** @var Sparklines $view */ + $view->config->title = Piwik::translate('BotTracking_ReportTitleChatbotsOverview'); + $view->config->addTranslations(Metrics::getMetricTranslations()); + $view->config->metrics_documentation = Metrics::getMetricDocumentation(); + + $order = 0; + foreach (Metrics::getSparklineMetricOrder() as $metric) { + if ( + \Piwik\Request::fromRequest()->getStringParameter('period', '') !== 'day' + && in_array($metric, [Metrics::METRIC_AI_CHATBOTS_UNIQUE_DOCUMENT_URLS, Metrics::METRIC_AI_CHATBOTS_UNIQUE_PAGE_URLS]) + ) { + continue; + } + $view->config->addSparklineMetric($metric, $order++); + } + + SegmentNotSupportedMessageHelper::addSegmentNotSupportedMessage($view); + } +} diff --git a/plugins/BotTracking/Reports/GetAIChatbotAIFavouredPages.php b/plugins/BotTracking/Reports/GetAIChatbotAIFavouredPages.php new file mode 100644 index 0000000000000000000000000000000000000000..dca72e120ea22a54458e2eb354111dfe1c78f331 --- /dev/null +++ b/plugins/BotTracking/Reports/GetAIChatbotAIFavouredPages.php @@ -0,0 +1,35 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; +use Piwik\Plugins\BotTracking\Columns\Metrics\DiscrepancyScore; + +class GetAIChatbotAIFavouredPages extends AbstractAIChatbotFavouredPagesReport +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsAIFavouredPagesTitle'); + $this->documentation = Piwik::translate('BotTracking_AIChatbotsAIFavouredPagesDocumentation'); + // Order 50 keeps this the last widget so it pairs with Human-Favoured (order 40) into the + // side-by-side row — see the layout contract in + // AbstractAIChatbotFavouredPagesReport::configureWidgets(). + $this->order = 50; + } + + protected function getDiscrepancyScoreVariant(): string + { + return DiscrepancyScore::VARIANT_AI_FAVOURED; + } +} diff --git a/plugins/BotTracking/Reports/GetAIChatbotBrokenContent.php b/plugins/BotTracking/Reports/GetAIChatbotBrokenContent.php new file mode 100644 index 0000000000000000000000000000000000000000..b137ebc14e8b72bf1807b9e50681b36e4204052b --- /dev/null +++ b/plugins/BotTracking/Reports/GetAIChatbotBrokenContent.php @@ -0,0 +1,90 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\DataTable; +use Piwik\Piwik; +use Piwik\Plugin\Report; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\BotTracking\Columns\ContentUrl; +use Piwik\Plugins\BotTracking\Columns\Metrics\PageNotFound404Requests; +use Piwik\Plugins\BotTracking\Columns\Metrics\ServerError5xxRequests; +use Piwik\Plugins\BotTracking\Columns\Metrics\TotalBrokenRequests; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Report\ReportWidgetFactory; +use Piwik\Widget\WidgetsList; + +class GetAIChatbotBrokenContent extends Report +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsBrokenContentTitle'); + $this->documentation = Piwik::translate('BotTracking_AIChatbotsBrokenContentDocumentation'); + $this->categoryId = 'General_AIAssistants'; + $this->subcategoryId = 'BotTracking_AIChatbotsContentRequests'; + $this->dimension = new ContentUrl(); + $this->metrics = [new TotalBrokenRequests(), new ServerError5xxRequests(), new PageNotFound404Requests()]; + $this->processedMetrics = []; + $this->order = 30; + $this->defaultSortColumn = Metrics::COLUMN_TOTAL_BROKEN_REQUESTS; + } + + public function configureView(ViewDataTable $view): void + { + parent::configureView($view); + + $view->config->setDefaultColumnsToDisplay( + [ + 'label', + Metrics::COLUMN_TOTAL_BROKEN_REQUESTS, + Metrics::COLUMN_PAGE_NOT_FOUND_404_REQUESTS, + Metrics::COLUMN_SERVER_ERROR_5XX_REQUESTS, + ], + false, + false + ); + + // Show-all-columns switches to the Visitor Engagement preset, which doesn't fit this schema. + $view->config->show_table_all_columns = false; + + // Insights and bar/pie/tag-cloud all assume visit metrics (nb_visits etc.) this report lacks, + // so they would render empty — the table is the only useful view. + $view->config->show_insights = false; + $view->config->show_bar_chart = false; + $view->config->show_pie_chart = false; + $view->config->show_tag_cloud = false; + + // Render URL labels as clickable links (scheme-less normalized URLs; prepend https://). + $view->config->filters[] = function (DataTable $table) { + foreach ($table->getRows() as $row) { + if ($row->isSummaryRow()) { + continue; + } + $label = $row->getColumn('label'); + if (is_string($label) && $label !== '') { + $row->setMetadata('url', 'https://' . $label); + } + } + }; + + SegmentNotSupportedMessageHelper::addSegmentNotSupportedMessage($view); + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory): void + { + // Rendered non-wide so it pairs side by side with the Documents report on the + // Content Requests page (consecutive non-wide widgets are grouped into two columns). + $widgetsList->addWidgetConfig($factory->createWidget()); + } +} diff --git a/plugins/BotTracking/Reports/GetAIChatbotContentDocuments.php b/plugins/BotTracking/Reports/GetAIChatbotContentDocuments.php new file mode 100644 index 0000000000000000000000000000000000000000..a24351b6e1f23326973c4002e6b1676f86a41bd7 --- /dev/null +++ b/plugins/BotTracking/Reports/GetAIChatbotContentDocuments.php @@ -0,0 +1,44 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; +use Piwik\Plugins\BotTracking\Columns\DocumentUrl; +use Piwik\Widget\WidgetsList; +use Piwik\Report\ReportWidgetFactory; + +class GetAIChatbotContentDocuments extends AbstractAIChatbotContentUrlReport +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsContentDocumentsTitle'); + $this->documentation = Piwik::translate('BotTracking_AIChatbotsContentDocumentsDocumentation'); + $this->dimension = new DocumentUrl(); + $this->order = 20; + } + + protected function getRequestsDocumentationKey(): string + { + // Scope the "Requests" tooltip to document URLs since this report covers only download actions. + return 'BotTracking_ColumnDocumentRequestsDocumentation'; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory): void + { + // Override the wide layout used by the shared base class (which keeps the Pages report + // full width) so this report pairs side by side with the Broken Pages & Documents report + // on the Content Requests page. + $widgetsList->addWidgetConfig($factory->createWidget()); + } +} diff --git a/plugins/BotTracking/Reports/GetAIChatbotContentPages.php b/plugins/BotTracking/Reports/GetAIChatbotContentPages.php new file mode 100644 index 0000000000000000000000000000000000000000..62d98f751848f1e798a1abddba46378817e71d03 --- /dev/null +++ b/plugins/BotTracking/Reports/GetAIChatbotContentPages.php @@ -0,0 +1,34 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; +use Piwik\Plugins\BotTracking\Columns\PageUrl; + +class GetAIChatbotContentPages extends AbstractAIChatbotContentUrlReport +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsContentPagesTitle'); + $this->documentation = Piwik::translate('BotTracking_AIChatbotsContentPagesDocumentation'); + $this->dimension = new PageUrl(); + $this->order = 10; + } + + protected function getRequestsDocumentationKey(): string + { + // Scope the "Requests" tooltip to page URLs since this report covers only page actions. + return 'BotTracking_ColumnPageRequestsDocumentation'; + } +} diff --git a/plugins/BotTracking/Reports/GetAIChatbotHumanFavouredPages.php b/plugins/BotTracking/Reports/GetAIChatbotHumanFavouredPages.php new file mode 100644 index 0000000000000000000000000000000000000000..65a268058ce33bbfb13a639d15486fb72a9d11d5 --- /dev/null +++ b/plugins/BotTracking/Reports/GetAIChatbotHumanFavouredPages.php @@ -0,0 +1,35 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; +use Piwik\Plugins\BotTracking\Columns\Metrics\DiscrepancyScore; + +class GetAIChatbotHumanFavouredPages extends AbstractAIChatbotFavouredPagesReport +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsHumanFavouredPagesTitle'); + $this->documentation = Piwik::translate('BotTracking_AIChatbotsHumanFavouredPagesDocumentation'); + // Order 40 keeps this the second-to-last widget so it pairs with AI-Favoured (order 50) + // into the side-by-side row — see the layout contract in + // AbstractAIChatbotFavouredPagesReport::configureWidgets(). + $this->order = 40; + } + + protected function getDiscrepancyScoreVariant(): string + { + return DiscrepancyScore::VARIANT_HUMAN_FAVOURED; + } +} diff --git a/plugins/BotTracking/Reports/GetAIChatbotRequests.php b/plugins/BotTracking/Reports/GetAIChatbotRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..e1feb141c2134fad249268a94224fe41ae92f572 --- /dev/null +++ b/plugins/BotTracking/Reports/GetAIChatbotRequests.php @@ -0,0 +1,92 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\Report; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\BotTracking\Columns\AIChatbotName; +use Piwik\Plugins\BotTracking\Columns\Metrics\AcquiredVisits; +use Piwik\Plugins\BotTracking\Columns\Metrics\DocumentRequests; +use Piwik\Plugins\BotTracking\Columns\Metrics\PageRequests; +use Piwik\Plugins\BotTracking\Columns\Metrics\Requests; +use Piwik\Plugins\BotTracking\Metrics; +use Piwik\Report\ReportWidgetFactory; +use Piwik\Widget\WidgetsList; +use Piwik\Plugins\BotTracking\Reports\SegmentNotSupportedMessageHelper; + +class GetAIChatbotRequests extends Report +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsReportTitle'); + $this->documentation = Piwik::translate('BotTracking_AIChatbotsReportDocumentation'); + $this->categoryId = 'General_AIAssistants'; + $this->subcategoryId = 'BotTracking_AIChatbotsOverview'; + $this->dimension = new AIChatbotName(); + $this->metrics = [ + new Requests(), + new PageRequests(), + new DocumentRequests(), + new AcquiredVisits(), + ]; + $this->processedMetrics = []; + $this->order = 30; + $this->defaultSortColumn = Metrics::COLUMN_ACQUIRED_VISITS; + if (\Piwik\Request::fromRequest()->getStringParameter('secondaryDimension', '') === 'documents') { + $this->actionToLoadSubTables = 'getDocumentUrlsForAIChatbot'; + } else { + $this->actionToLoadSubTables = 'getPageUrlsForAIChatbot'; + } + } + + public function configureView(ViewDataTable $view): void + { + parent::configureView($view); + + $view->config->show_table_all_columns = false; + $view->config->show_insights = false; + + SegmentNotSupportedMessageHelper::addSegmentNotSupportedMessage($view); + + $view->config->setDefaultColumnsToDisplay( + ['label', Metrics::COLUMN_REQUESTS, Metrics::COLUMN_PAGE_REQUESTS, Metrics::COLUMN_DOCUMENT_REQUESTS, Metrics::COLUMN_ACQUIRED_VISITS], + false, + false + ); + + // only show request count for flat table, as subtable doesn't have other metrics + if ((int)$view->requestConfig->getRequestParam('flat') === 1) { + $view->config->setDefaultColumnsToDisplay( + ['label', Metrics::COLUMN_REQUESTS], + false, + false + ); + } + + $secondaryDimensions = [ + 'pages' => Piwik::translate('BotTracking_ColumnPageRequests'), + 'documents' => Piwik::translate('BotTracking_ColumnDocumentRequests'), + ]; + $view->config->setSecondaryDimensions($secondaryDimensions, 'pages'); + } + + /** + * @return void + */ + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + $widgetsList->addWidgetConfig($factory->createWidget()->setIsWide()); + } +} diff --git a/plugins/BotTracking/Reports/GetAIChatbotsRealTime.php b/plugins/BotTracking/Reports/GetAIChatbotsRealTime.php new file mode 100644 index 0000000000000000000000000000000000000000..f7dfde43443ef07ed464ca6277556e8070ee36da --- /dev/null +++ b/plugins/BotTracking/Reports/GetAIChatbotsRealTime.php @@ -0,0 +1,34 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; + +class GetAIChatbotsRealTime extends AbstractAIChatbotsRealTimeChatbotsReport +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsLast30MinutesTitle'); + $this->documentation = Piwik::translate('BotTracking_AIChatbotsLast30MinutesDocumentation'); + $this->order = 10; + } + + /** + * @return array<int, array{name: string, documentation: string, lastMinutes: int, order: int}> + */ + protected function getWidgetDefinitions(): array + { + return AIChatbotsRealTimeWidgets::getChatbotWidgets(); + } +} diff --git a/plugins/BotTracking/Reports/GetDocumentUrlsForAIChatbot.php b/plugins/BotTracking/Reports/GetDocumentUrlsForAIChatbot.php new file mode 100644 index 0000000000000000000000000000000000000000..d0f3d3aeddc22675e7ae519e92ffe5e7fb2a2c51 --- /dev/null +++ b/plugins/BotTracking/Reports/GetDocumentUrlsForAIChatbot.php @@ -0,0 +1,32 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\Report; +use Piwik\Plugins\BotTracking\Columns\DocumentUrl; +use Piwik\Plugins\BotTracking\Columns\Metrics\Requests; + +class GetDocumentUrlsForAIChatbot extends Report +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsReportTitle'); + $this->categoryId = 'General_AIAssistants'; + $this->metrics = [new Requests()]; + $this->processedMetrics = []; + $this->dimension = new DocumentUrl(); + $this->isSubtableReport = true; + } +} diff --git a/plugins/BotTracking/Reports/GetPageUrlsForAIChatbot.php b/plugins/BotTracking/Reports/GetPageUrlsForAIChatbot.php new file mode 100644 index 0000000000000000000000000000000000000000..55d830cfd640b06e0059443c45b7edd424f7dd57 --- /dev/null +++ b/plugins/BotTracking/Reports/GetPageUrlsForAIChatbot.php @@ -0,0 +1,32 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; +use Piwik\Plugin\Report; +use Piwik\Plugins\BotTracking\Columns\Metrics\Requests; +use Piwik\Plugins\BotTracking\Columns\PageUrl; + +class GetPageUrlsForAIChatbot extends Report +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_AIChatbotsReportTitle'); + $this->categoryId = 'General_AIAssistants'; + $this->metrics = [new Requests()]; + $this->processedMetrics = []; + $this->dimension = new PageUrl(); + $this->isSubtableReport = true; + } +} diff --git a/plugins/BotTracking/Reports/GetTopPageUrlsRealTime.php b/plugins/BotTracking/Reports/GetTopPageUrlsRealTime.php new file mode 100644 index 0000000000000000000000000000000000000000..5758abe32bf69eb9c5de010cc1f9d6f2014f81d9 --- /dev/null +++ b/plugins/BotTracking/Reports/GetTopPageUrlsRealTime.php @@ -0,0 +1,34 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\Piwik; + +class GetTopPageUrlsRealTime extends AbstractAIChatbotsRealTimeTopPageUrlsReport +{ + protected function init(): void + { + parent::init(); + + $this->name = Piwik::translate('BotTracking_TopPageUrlsLast30MinutesTitle'); + $this->documentation = Piwik::translate('BotTracking_TopPageUrlsLast30MinutesDocumentation'); + $this->order = 30; + } + + /** + * @return array<int, array{name: string, documentation: string, lastMinutes: int, order: int}> + */ + protected function getWidgetDefinitions(): array + { + return AIChatbotsRealTimeWidgets::getTopPageUrlWidgets(); + } +} diff --git a/plugins/BotTracking/Reports/SegmentNotSupportedMessageHelper.php b/plugins/BotTracking/Reports/SegmentNotSupportedMessageHelper.php new file mode 100644 index 0000000000000000000000000000000000000000..1d96fe030b956691bcf556f77c804f225a04a0cb --- /dev/null +++ b/plugins/BotTracking/Reports/SegmentNotSupportedMessageHelper.php @@ -0,0 +1,27 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Reports; + +use Piwik\API\Request; +use Piwik\Piwik; +use Piwik\Plugin\ViewDataTable; + +class SegmentNotSupportedMessageHelper +{ + public static function addSegmentNotSupportedMessage(ViewDataTable $view): void + { + if (!empty(Request::getRawSegmentFromRequest())) { + $message = '<p class="alert alert-info">' . Piwik::translate('BotTracking_SegmentNotSupported') . '</p>'; + $view->config->show_footer_message = $message; + } + } +} diff --git a/plugins/BotTracking/Tracker/BotRequestProcessor.php b/plugins/BotTracking/Tracker/BotRequestProcessor.php new file mode 100644 index 0000000000000000000000000000000000000000..d196dccab1a10957b07af35d2a89579d104030db --- /dev/null +++ b/plugins/BotTracking/Tracker/BotRequestProcessor.php @@ -0,0 +1,154 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Tracker; + +use Piwik\Date; +use Piwik\Log\LoggerInterface; +use Piwik\Plugins\BotTracking\BotDetector; +use Piwik\Plugins\BotTracking\Dao\BotRequestsDao; +use Piwik\Tracker\Action; +use Piwik\Tracker\PageUrl; +use Piwik\Tracker\Request; +use Piwik\Tracker\TableLogAction; + +/** + * Handles bot tracking requests + */ +class BotRequestProcessor extends \Piwik\Tracker\BotRequestProcessor +{ + /** + * @var BotRequestsDao + */ + private $dao; + + /** + * @var LoggerInterface + */ + private $logger; + + public function __construct(BotRequestsDao $dao, LoggerInterface $logger) + { + $this->dao = $dao; + $this->logger = $logger; + } + + public function handleRequest(Request $request): bool + { + $userAgent = $request->getUserAgent(); + $botDetector = new BotDetector($userAgent); + + if (!$botDetector->isBot()) { + return false; + } + + $botInfo = $botDetector->getDetectionResult(); + + if (empty($botInfo)) { + return false; + } + + try { + $idSite = $request->getIdSite(); + + $timestamp = $request->getCurrentTimestamp(); + $serverTime = Date::factory($timestamp)->getDatetime(); + + $idActionUrl = $this->getActionId($request); + + $rawParams = new \Piwik\Request($request->getRawParams()); + + $httpStatusCode = $rawParams->getIntegerParameter('http_status', -1); + if ($httpStatusCode < 0) { + $httpStatusCode = null; + } + + $responseSizeBytes = $rawParams->getIntegerParameter('bw_bytes', -1); + if ($responseSizeBytes < 0) { + $responseSizeBytes = null; + } + + $serverTimeMs = $rawParams->getIntegerParameter('pf_srv', -1); + if ($serverTimeMs < 0) { + $serverTimeMs = null; + } + + $source = $rawParams->getStringParameter('source', ''); + if ($source === '') { + $source = null; + } + + $data = [ + 'idsite' => $idSite, + 'server_time' => $serverTime, + 'idaction_url' => $idActionUrl, + 'bot_name' => $botInfo['bot_name'], + 'bot_type' => $botInfo['bot_type'], + 'http_status_code' => $httpStatusCode, + 'response_size_bytes' => $responseSizeBytes, + 'response_time_ms' => $serverTimeMs, + 'source' => $source, + ]; + + $idRequest = $this->dao->insert($data); + + $this->logger->debug('Bot request recorded: idrequest=' . $idRequest); + } catch (\Exception $e) { + $this->logger->debug('Error recording bot request: ' . $e->getMessage()); + // Don't throw - we don't want to break tracking for other processors + } + + return true; + } + + /** + * Get or create action ID for the URL + */ + private function getActionId(Request $request): ?int + { + $url = $request->getParam('url'); + $actionType = Action::TYPE_PAGE_URL; + + $downloadUrl = $request->getParam('download'); + if ($downloadUrl !== false && $downloadUrl !== '') { + $url = $downloadUrl; + $actionType = Action::TYPE_DOWNLOAD; + } + + if ($url === false || $url === '') { + return null; + } + + if ($actionType === Action::TYPE_PAGE_URL) { + // Apply the same excluded query parameter logic as normal page tracking. + // Download URLs are intentionally left untouched, matching ActionDownloadUrl. + $url = PageUrl::excludeQueryParametersFromUrl($url, $request->getIdSite()); + } + + $urlInfo = PageUrl::normalizeUrl($url); + + $actionsToLookup = [ + [$urlInfo['url'], $actionType, $urlInfo['prefixId']], + ]; + + try { + [$actionId] = TableLogAction::loadIdsAction($actionsToLookup); + + if (isset($actionId)) { + return (int) $actionId; + } + } catch (\Exception $e) { + $this->logger->debug('Error resolving action ID: ' . $e->getMessage()); + } + + return null; + } +} diff --git a/plugins/BotTracking/Widgets/NoRecentRequests.php b/plugins/BotTracking/Widgets/NoRecentRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..83e93fb137f3195895d02f8212897a0b45171b9d --- /dev/null +++ b/plugins/BotTracking/Widgets/NoRecentRequests.php @@ -0,0 +1,42 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Widgets; + +use Piwik\Widget\Widget; +use Piwik\Widget\WidgetConfig; + +class NoRecentRequests extends Widget +{ + public static function configure(WidgetConfig $config) + { + self::configureMessageWidget($config, 'BotTracking_AIChatbotsOverview', 'noRecentRequestsMessage'); + } + + /** + * Shared config for the "no recent AI bot requests" message. It tops each AI Chatbots page as + * its own widget; the showNoRecentRequestsMessage middleware hides it once recent requests exist. + */ + public static function configureMessageWidget(WidgetConfig $config, string $subcategoryId, string $action): void + { + $config + ->setName('BotTracking_NoRecentRequestsWidgetTitle') + ->setCategoryId('General_AIAssistants') + ->setSubcategoryId($subcategoryId) + ->setModule('BotTracking') + ->setAction($action) + ->setClientSideComponent('BotTracking', 'NoRecentRequestsWidget') + ->setMiddlewareParameters(['module' => 'BotTracking', 'action' => 'showNoRecentRequestsMessage']) + ->setIsWide() + ->setOrder(0) + ->setIsNotWidgetizable(); + } +} diff --git a/plugins/BotTracking/Widgets/NoRecentRequestsContentRequests.php b/plugins/BotTracking/Widgets/NoRecentRequestsContentRequests.php new file mode 100644 index 0000000000000000000000000000000000000000..36648bbb1fd633cf20940ef4cb5a459bc5b7dce1 --- /dev/null +++ b/plugins/BotTracking/Widgets/NoRecentRequestsContentRequests.php @@ -0,0 +1,31 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Widgets; + +use Piwik\Widget\Widget; +use Piwik\Widget\WidgetConfig; + +/** + * Places the "no recent AI bot requests" message on the Content Requests page too, so it matches the + * Overview when nothing has been tracked recently. A distinct action gives it its own widget id. + */ +class NoRecentRequestsContentRequests extends Widget +{ + public static function configure(WidgetConfig $config) + { + NoRecentRequests::configureMessageWidget( + $config, + 'BotTracking_AIChatbotsContentRequests', + 'noRecentRequestsMessageContentRequests' + ); + } +} diff --git a/plugins/BotTracking/Widgets/NoRecentRequestsRealtime.php b/plugins/BotTracking/Widgets/NoRecentRequestsRealtime.php new file mode 100644 index 0000000000000000000000000000000000000000..cf51d8dd28b935ce140d27c6e385e72a2a1abc1a --- /dev/null +++ b/plugins/BotTracking/Widgets/NoRecentRequestsRealtime.php @@ -0,0 +1,31 @@ +<?php + +/** + * Matomo - free/libre analytics platform + * + * @link https://matomo.org + * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +declare(strict_types=1); + +namespace Piwik\Plugins\BotTracking\Widgets; + +use Piwik\Widget\Widget; +use Piwik\Widget\WidgetConfig; + +/** + * Places the "no recent AI bot requests" message on the Real-time page too, so it matches the + * Overview when nothing has been tracked recently. A distinct action gives it its own widget id. + */ +class NoRecentRequestsRealtime extends Widget +{ + public static function configure(WidgetConfig $config) + { + NoRecentRequests::configureMessageWidget( + $config, + 'BotTracking_AIChatbotsRealtime', + 'noRecentRequestsMessageRealtime' + ); + } +} diff --git a/plugins/BotTracking/changes.json b/plugins/BotTracking/changes.json new file mode 100644 index 0000000000000000000000000000000000000000..86854889dc7d4ec0da01845f41bbfbfc577d7b15 --- /dev/null +++ b/plugins/BotTracking/changes.json @@ -0,0 +1,16 @@ +[ + { + "title": "New: AI Chatbot Tracking - Content Requests", + "description": "Gain deeper visibility into how AI chatbots interact with your content. See which pages and documents AI systems access, compare AI and human content engagement, and identify content that attracts different levels of interest from each audience.", + "link_name": "Explore AI Content Requests or set up AI Chatbot tracking", + "link": "/index.php?module=CoreHome&action=index&category=General_AIAssistants&subcategory=BotTracking_AIChatbotsContentRequests&idSite=1", + "version": "5.12.0" + }, + { + "title": "AI Chatbot Tracking", + "description": "Matomo introduces AI Chatbot tracking. See how AI Chatbots such as ChatGPT access your site and content, and gain insights that traditional analytics usually miss. These reports use server-side logs to identify chatbot traffic. To enable this feature, setup via a supported server integration is required.", + "link_name": "Learn here how to set up chatbot tracking", + "link": "/index.php?module=BotTracking&action=siteWithoutData&idSite=1", + "version": "5.8.0" + } +] diff --git a/plugins/BotTracking/config/config.php b/plugins/BotTracking/config/config.php new file mode 100644 index 0000000000000000000000000000000000000000..0b67a5fe4742a02757d939368cc05d4c246502f8 --- /dev/null +++ b/plugins/BotTracking/config/config.php @@ -0,0 +1,3 @@ +<?php + +return []; diff --git a/plugins/BotTracking/config/tracker.php b/plugins/BotTracking/config/tracker.php new file mode 100644 index 0000000000000000000000000000000000000000..0b67a5fe4742a02757d939368cc05d4c246502f8 --- /dev/null +++ b/plugins/BotTracking/config/tracker.php @@ -0,0 +1,3 @@ +<?php + +return []; diff --git a/plugins/BotTracking/lang/am.json b/plugins/BotTracking/lang/am.json new file mode 100644 index 0000000000000000000000000000000000000000..f6853e487d2b7ea53c397cd8e1aebf4d7694267c --- /dev/null +++ b/plugins/BotTracking/lang/am.json @@ -0,0 +1,5 @@ +{ + "BotTracking": { + "SiteWithoutDataBackToMatomo": "ወደፒዊክ ተመለስ" + } +} diff --git a/plugins/BotTracking/lang/ar.json b/plugins/BotTracking/lang/ar.json new file mode 100644 index 0000000000000000000000000000000000000000..bf9a9ac3dfe4cae8e802ad0799c7285afe36f54d --- /dev/null +++ b/plugins/BotTracking/lang/ar.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "رابط الصفحة", + "SiteWithoutDataBackToMatomo": "العودة إلى Matomo", + "AIChatbotsContentPagesTitle": "الصفحات", + "ContentUrl": "الرابط" + } +} diff --git a/plugins/BotTracking/lang/az.json b/plugins/BotTracking/lang/az.json new file mode 100644 index 0000000000000000000000000000000000000000..1430c10ff2489a5ffbfbdc77aa291b782bb59cca --- /dev/null +++ b/plugins/BotTracking/lang/az.json @@ -0,0 +1,5 @@ +{ + "BotTracking": { + "PageUrl": "Sayfa Adresi" + } +} diff --git a/plugins/BotTracking/lang/be.json b/plugins/BotTracking/lang/be.json new file mode 100644 index 0000000000000000000000000000000000000000..2651e50141e6abf00c168e68c7a260b7a0729340 --- /dev/null +++ b/plugins/BotTracking/lang/be.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "URL старонкі", + "SiteWithoutDataBackToMatomo": "Вярнуцца да Matomo", + "AIChatbotsContentPagesTitle": "Старонкі", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/bg.json b/plugins/BotTracking/lang/bg.json new file mode 100644 index 0000000000000000000000000000000000000000..4b279c6413cf79128ee98a3b382ca9f2ce6c2d4a --- /dev/null +++ b/plugins/BotTracking/lang/bg.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "Страница URL", + "NoDataHttpTrackingApi": "API за проследяване на HTTP", + "SiteWithoutDataBackToMatomo": "Върнете се в Matomo", + "AIChatbotsContentPagesTitle": "Страници", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/bn.json b/plugins/BotTracking/lang/bn.json new file mode 100644 index 0000000000000000000000000000000000000000..cbc6f60e69dfadd34662205ef44daea8be4eb193 --- /dev/null +++ b/plugins/BotTracking/lang/bn.json @@ -0,0 +1,5 @@ +{ + "BotTracking": { + "SiteWithoutDataBackToMatomo": "Matomo এ ফিরে যান" + } +} diff --git a/plugins/BotTracking/lang/bs.json b/plugins/BotTracking/lang/bs.json new file mode 100644 index 0000000000000000000000000000000000000000..6c9ba63a05e8d76992ab60a914a40c039a6311dd --- /dev/null +++ b/plugins/BotTracking/lang/bs.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "URL stranice", + "SiteWithoutDataBackToMatomo": "Nazad na Matomo", + "AIChatbotsContentPagesTitle": "Stranice", + "ContentUrl": "Veza" + } +} diff --git a/plugins/BotTracking/lang/ca.json b/plugins/BotTracking/lang/ca.json new file mode 100644 index 0000000000000000000000000000000000000000..b5b63605bfba31a582b72f30845eb237bf1e5587 --- /dev/null +++ b/plugins/BotTracking/lang/ca.json @@ -0,0 +1,12 @@ +{ + "BotTracking": { + "PluginDescription": "Feu un seguiment i analitzeu les visites de bots d'IA com ara ChatGPT-User i similars. Aquest complement us ajuda a entendre com les eines basades en IA interactuen amb el vostre lloc web registrant i informant de les sol·licituds activades per bots.", + "PageUrl": "URL de la pàgina", + "NoDataHttpTrackingApi": "API de seguiment HTTP", + "SiteWithoutDataBackToMatomo": "Torna al Matomo", + "SiteWithoutDataInstallWithX": "Instal·la amb %1$s", + "SiteWithoutDataStepByStepGuide": "Guia pas a pas", + "AIChatbotsContentPagesTitle": "Pàgines", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/cs.json b/plugins/BotTracking/lang/cs.json new file mode 100644 index 0000000000000000000000000000000000000000..25b99ce01e4a5d17a79677a680244b2a30b9c067 --- /dev/null +++ b/plugins/BotTracking/lang/cs.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "URL stránky", + "NoDataHttpTrackingApi": "HTTP Tracking API", + "SiteWithoutDataBackToMatomo": "Zpět do Matoma", + "AIChatbotsContentPagesTitle": "Stránky", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/cy.json b/plugins/BotTracking/lang/cy.json new file mode 100644 index 0000000000000000000000000000000000000000..e7f1dab5eea651e057dda082f428e138f8510859 --- /dev/null +++ b/plugins/BotTracking/lang/cy.json @@ -0,0 +1,5 @@ +{ + "BotTracking": { + "SiteWithoutDataBackToMatomo": "Yn ôl i" + } +} diff --git a/plugins/BotTracking/lang/da.json b/plugins/BotTracking/lang/da.json new file mode 100644 index 0000000000000000000000000000000000000000..2f1d90b4127c5235fef4e53c42a5875060b30fdc --- /dev/null +++ b/plugins/BotTracking/lang/da.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "Side URL", + "NoDataHttpTrackingApi": "HTTP-sporings-API", + "SiteWithoutDataBackToMatomo": "Tilbage til Matomo", + "AIChatbotsContentPagesTitle": "Sider", + "ContentUrl": "Netadresse" + } +} diff --git a/plugins/BotTracking/lang/de.json b/plugins/BotTracking/lang/de.json new file mode 100644 index 0000000000000000000000000000000000000000..4495e00a2f593ccbfac6313044ddf1996f14bdfb --- /dev/null +++ b/plugins/BotTracking/lang/de.json @@ -0,0 +1,21 @@ +{ + "BotTracking": { + "PageUrl": "Seiten-URL", + "NoDataHttpTrackingApi": "HTTP Tracking API", + "SiteWithoutDataBackToMatomo": "Zurück zu Matomo", + "SiteWithoutDataInstallWithX": "Mit %1$s installieren", + "SiteWithoutDataStepByStepGuide": "Schritt-für-Schritt-Anleitung", + "AIChatbotsOverview": "AI Chatbots Übersicht", + "AIChatbotsReportTitle": "KI Chatbots", + "ColumnAIChatbotName": "KI Chatbot Name", + "ColumnDocumentRequests": "Dokumentenanfragen", + "ColumnDocumentRequestsDocumentation": "Gesamtanzahl der vom Bot gestellten Anfragen an Dokument-URLs, die im ausgewählten Zeitraum aufgezeichnet wurden.", + "ColumnPageRequests": "Seitenanfragen", + "ColumnRequests": "Anfragen", + "ColumnServerErrorRequests": "Serverfehler (5xx) Anfragen", + "ColumnUniqueAiChatbots": "Einzigartige KI-Chatbots", + "ColumnUniqueDocumentUrls": "Einzigartige Dokument-URLs", + "AIChatbotsContentPagesTitle": "Seiten", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/dv.json b/plugins/BotTracking/lang/dv.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/plugins/BotTracking/lang/dv.json @@ -0,0 +1 @@ +{} diff --git a/plugins/BotTracking/lang/el.json b/plugins/BotTracking/lang/el.json new file mode 100644 index 0000000000000000000000000000000000000000..d9cfdb398a9248825681fa0c95393c828973ba30 --- /dev/null +++ b/plugins/BotTracking/lang/el.json @@ -0,0 +1,113 @@ +{ + "BotTracking": { + "PluginDescription": "Ιχνηλάτηση και ανάλυση επισκέψεων από chatbot ΤΝ όπως το ChatGPT-User και παρόμοια. Το πρόσθετο σας βοηθά να κατανοήσετε πώς τα υποβοηθούμενα από ΤΝ εργαλεία αλληλεπιδρούν με τον ιστοτόπο σας καταγράφοντας και κάνοντας αναφορά των αιτήσεων που γίνονται από chatbot.", + "PageUrl": "URL σελίδας", + "ColumnAcquiredVisits": "Ληφθείσες επισκέψεις", + "ColumnAcquiredVisitsDocumentation": "Επισκέψεις που ξεκίνησαν αφού κάποιος έκανε κλικ από ένα chatbot ΤΝ (Αναφορείς → Βοηθοί ΤΝ).", + "ColumnDocumentRequests": "Αιτήσεις Εγγράφων", + "ColumnDocumentRequestsDocumentation": "Συνολικός αριθμός αιτήσεων από μποτ για διευθύνσεις URL εγγράφων που καταγράφηκαν στην επιλεγμένη περίοδο.", + "ColumnPageRequests": "Αιτήσεις σελίδων", + "ColumnPageRequestsDocumentation": "Συνολικός αριθμός αιτήσεων μποτ για διευθύνσεις URL σελίδων που καταγράφηκαν στην επιλεγμένη περίοδο.", + "ColumnRequests": "Αιτήσεις", + "ColumnRequestsDocumentation": "Συνολικός αριθμός αιτήσεων μποτ που καταγράφηκαν στην επιλεγμένη περίοδο. Περιλαμβάνει τόσο τις διευθύνσεις URL για σελίδες και έγγραφα.", + "ColumnUniquePageUrls": "Μοναδικές διευθύνσεις URL σελίδων", + "ColumnUniquePageUrlsDocumentation": "Μοναδικές διευθύνσεις URL που αιτήθηκαν τα chatbot ΤΝ.", + "ColumnUniqueDocumentUrls": "Μοναδικές διευθύνσεις URL εγγράφων", + "ColumnUniqueDocumentUrlsDocumentation": "Μοναδικές διευθύνσεις URL εγγράφων και μεταφορτώσεων που αιτούνται τα chatbot ΤΝ.", + "ColumnUniqueAiChatbots": "Μοναδικά bot ΤΝ", + "ColumnUniqueAiChatbotsDocumentation": "Chatbot ΤΝ που εντοπίστηκαν στην επιλεγμένη περίοδο.", + "ColumnNotFoundRequests": "Αιτήσεις μη εύρεσης", + "ColumnNotFoundRequestsDocumentation": "Αριθμός αιτήσεων από μποτ που επέστρεψαν απόκριση HTTP 404 ή 410, συμπεριλαμβανομένου τις διευθύνσεις URL των εγγράφων.", + "ColumnServerErrorRequests": "Αιτήσεις σφαλμάτων διακομιστή (5xx)", + "ColumnServerErrorRequestsDocumentation": "Αριθμός αιτήσεων μποτ που επέστρεψαν απόκριση HTTP 5xx, συμπεριλαμβανομένου τις διευθύνσεις URL των εγράφων.", + "ColumnClickThroughRate": "Ρυθμός κλικ-διαμέσου", + "ColumnClickThroughRateDocumentation": "Μερίδιο των αιτήσεων από chatbot ΤΝ που είχαν ως αποτέλεσμα σε επισκέψεις που αποκτήθηκαν (αποκτημένες επισκέψεις ÷ αιτήσεις).", + "DocumentUrl": "Διεύθυνση URL εγγράφου", + "SegmentNotSupported": "Η αναφορά δεν υποστηρίζει τμηματοποίηση. Τα δεδομένα που εμφανίζονται είναι τα πρότυπα, χωρίς τμηματοποίηση δεδομένα αναφοράς.", + "NoDataHttpTrackingApi": "HTTP API Παρακολούθησης", + "SiteWithoutDataBackToMatomo": "Επιστροφή στο Matomo", + "SiteWithoutDataInstallWithX": "Εγκατάσταση με %1$s", + "SiteWithoutDataStepByStepGuide": "Οδηγός βήμα-βήμα", + "AIChatbotsOverview": "Σύνοψη chatbot ΤΝ", + "AIChatbotsOverviewHelp2": "Πρέπει να σημειωθεί ότι καμία από αυτές τις σελίδες δεν έχει προβληθεί από ανθρώπους με τον κλασικό τρόπο — όλες οι αιτήσεις προέρχονται από chatbox ΤΝ που αντλούν αυτόματα το περιεχόμενο.", + "AIChatbotsOverviewHelp3": "Αυτή τη στιγμή, οι αναφορές περιλαμβάνουν αποκλειστικά αιτήσεις από chatbot ΤΝ που δεν εκτελούν JavaScript. Δεν περιλαμβάνουν κίνηση από ανιχνευτές ΤΝ για την εκπαίδευση μοντέλων ΤΝ ή από πράκτορες ΤΝ ικανούς να εκτελούν JavaScript.", + "AIChatbotsReportDocumentation": "Ανακαλύψτε ποια chatbot ΤΝ ανιχνεύουν τον ιστοτόπο σας και πόσες επισκέψεις παράγει το καθένα. Πατήστε σε ένα chatbot για να δείτε τις πιο συχνά αιτούμενες σελίδες ή έγγραφα.", + "AIChatbotsReportTitle": "Chatbot ΤΝ", + "ChatbotsOverTimeReportDocumentation": "Αυτή η αναφορά εμφανίζει σε γραφική παράσταση την δραστηριότητα των chatbot ΤΝ. Κάντε κλικ σε οποιαδήποτε μετρική για ν α μεγεθύνετε και να δείτε την υποκείμενη τάση.", + "ColumnAIChatbotName": "Όνομα chatbot ΤΝ", + "DetectingYourSite": "Γίνεται εντοπισμός του ιστοτόπου σας…", + "SiteWithoutDataRecommendationText": "Έχουμε εντοπίσεις ότι ο ιστοτόπος σας χρησιμοποιεί %1$s, οπότε μπορείτε να ορίσετε για ιχνηλάτηση ένα chatbot ΤΝ χρησιμοποιώντας την επίσημη %1$sενσωμάτωσή μας.", + "SiteWithoutDataOtherInstallMethodsIntro": "Παρέχουμε πολλαπλά μονοπάτια ιχνηλάτησης - επιλέξτε αυτό που ταιριάζει με την παραμετροποίησή σας.", + "SiteWithoutDataOtherInstallMethods": "Άλλοι μέθοδοι για την ιχνηλάτηση των chatbot ΤΝ", + "SiteWithoutDataNotYetReady": "Δεν είστε ακόμη έτοιμοι να παραμετροποιήσετε την ιχνηλάτηση των chatbot ΤΝ;", + "SiteWithoutDataInstallWithXRecommendation": "Εγκαταστήστε την ιχνηλάτηση των chatbot ΤΝ με το %1$s (συνιστάται για εσάς)", + "SiteWithoutDataChooseTrackingMethodPreamble2": "Τα chatbot ΤΝ τυπικά δεν εκτελούν JavaScript. Ως αποτέλεσμα, δεν ιχνηλατούνται εξ' ορισμού από αναλυτικά βασισμένα σε JavaScript και απαιτείται πρόσθετη παραμετροποίηση ιχνηλάτησης.", + "SiteWithoutDataChooseTrackingMethodPreamble1": "Ιχνηλατήστε τα chatbot ΤΝ που προσπελαύνουν τον ιστοτόπο σας για να κατανοήσετε πώς ανακαλύπτεται το περιεχόμενο και καταναλώνεται από βοηθούς βασισμένους σε ΤΝ.", + "SiteWithoutDataChooseTrackingMethod": "Επιλέξτε την μέθοδο Ιχνηλάτησης των chatbot ΤΝ", + "ReportTitleChatbotsOverview": "Σύνοψη chatbot ΤΝ", + "ReportTitleChatbotsOverTime": "Τα chatbot ΤΝ μέσα στο χρόνο", + "NoRecentRequestsWidgetTitle": "Chatbot ΤΝ - Χωρίς Πρόσφατες Αιτήσεις", + "NoRecentAIBotRequests": "%1$sΔεν συλλέχθηκαν δεδομένα τις τελευταίες 7 ημέρες.%2$s %3$sΚαθορίστε την ιχνηλάτηση των chatbot ΤΝ τώρα%4$s.", + "NoDataWordPressInstructions4": "Επιβεβαιώστε την παραμετροποίηση ελέγχοντας ότι εμφανίζονται επισκέψεις από τα chatbot ΤΝ στις αναφορές ιχνηλάτησης.", + "NoDataWordPressInstructions3": "Εισαγάγετε τη διεύθυνση URL του Matomo και το Αναγνωριστικό Ιστοτόπου.", + "NoDataWordPressInstructions2": "Εγκατάσταση και ενεργοποίηση του πρόσθετου.", + "AIChatbotsOverviewHelp1": "Η σελίδα σύνοψης των chatbot ΤΝ παρέχει πληροφορία γύρω από την κίνηση του ιστοτόπου που έχει ως πηγή τα chatbot ΤΝ όπως το ChatGPT και άλλους βοηθούς βασισμένους σε LLM. Αυτές οι αναφορές ιχνηλατούν μετρικές κλειδιά συμπεριλαμβανομένου του αριθμού των αιτήσεων που έκαναν αυτά τα chatbot, τις σελίδες και τα έγγραφα που προσπελαύνουν και τυχόν λάθη που συναντούν. Παρέχουν επίσης λεπτομερή ανάλυση δείχνοντας ποια chatbot επισκέπτονται συγκεκριμένες διευθύνσεις URL σελίδων, βοηθώντας σας να κατανοήσετε πως τα chatbot ΤΝ αλληλεπιδρούν με το περιεχόμενό σας και να εντοπίσετε ευκαιρίες για να βελτιώσετε την ορατότητα και προσπελασιμότητα για τους υποβοηθούμενους από ΤΝ χρήστες.", + "NoDataAmazonCloudFrontInstructions1": "Αυτή η ενσωμάτωση χρησιμοποιεί το AWS Lambda που διαβάζει τα ημερολόγια προσπέλασης στο Amazon CloudFront από το S3 και στέλνει στο Matomo χτυπήματα με μαζικό τρόπο. Ο κώδικας ιχνηλάτησης συντηρείται εξωτερικά, οπότε ακολουθήστε το αρχείο README του αποθετηρίου για τις λεπτομέρειες της τρέχουσας παραμετροποίησης και εγκατάστασης.", + "NoDataAmazonCloudFrontInstructions2": "Αξιοποίηση του Lambda (Node.js) και καθορισμός της διεύθυνσης URL του Matomo και του αναγνωριστικού ιστοτόπου.", + "NoDataAmazonCloudFrontInstructions3": "Προσαρμόστε μια σκανδάλη S3 για τα αντικείμενα ημερολογίου του Amazon CloudFront (για παράδειγμα, ObjectCreated στα ημερολόγια .gz).", + "NoDataAmazonCloudFrontInstructions4": "Επαληθεύστε την παραμετροποίηση ελέγχοντας τα χτυπήματα από τα chatbot ΤΝ που εμφανίζονται στις αναφορές ιχνηλάτησης.", + "NoDataCloudflareInstructions1": "Αυτή η ενσωμάτωση χρησιμοποιεί ένα Cloudflare Worker που λειτουργεί ως ενδιάμεσος προς τις αιτήσεις πηγής σας και με ασύγχρονο τρόπο αποστέλλει στο Matomo τα χτυπήματα χωρίς καθυστέρηση στις απαντήσεις. Ο κώδικας ιχνηλάτησης συντηρείται εξωτερικά, οπότε ακολουθήστε τις οδηγίες στο αρχείο README του αποθετηρίου για πληροφορίες σχετικά με την τρέχουσα παραμετροποίηση και εγκατάσταση.", + "NoDataCloudflareInstructions2": "Αξιοποίηση του Worker (Node.js) και καθορισμός της διεύθυνσης URL του Matomo και του αναγνωριστικού ιστοτόπου.", + "NoDataCloudflareInstructions3": "Προσαρμόστε το Worker στη ζώνη ή στη διαδρομή σας ώστε να εκτελείται ενδιάμεσα με τις αιτήσεις.", + "NoDataCloudflareInstructions4": "Επαληθεύστε την παραμετροποίηση ελέγχοντας τα χτυπήματα από τα chatbot ΤΝ που εμφανίζονται στις αναφορές ιχνηλάτησης.", + "NoDataHttpTrackingApiDescription": "Το HTTP Tracking API σας επιτρέπει να ιχνηλατείτε αιτήσεις από μποτ ΤΝ απευθείας όταν χρειάζεστε μια προσαρμοσμένη ενσωμάτωση. Ξεκινήστε με τη %1$sγνωσιακή βάση του Tracking API%2$s και χρησιμοποιήστε την %3$sαναφορά του HTTP Tracking API%4$s για τις πιο πρόσφατες παραμέτρους και συμπεριφορές για την ιχνηλάτηση των μποτ ΤΝ.", + "NoDataLinkKnowledgeBase": "Τεκμηρίωση: %1$sΓνωσιακή βάση του Matomo%2$s", + "NoDataLinkSourceCode": "Πηγαίος κώδικας: %1$sΑποθετήριο GitHub%2$s", + "NoDataMissingOneContactUs": "Λείπει κάτι; Επικοινωνήστε μαζί μας", + "NoDataWordPressInstructions1": "Η ενσωμάτωση WordPress προσθέτει μια μικρή γέφυρα που στέλνει τα σήματα από μποτ ΤΝ στο Matomo. Καθώς ο ιχνηλάτης συντηρείται εξωτερικά, το αρχείο README στον πηγαίο κώδικα αποτελεί την πηγή αλήθειας για τα τρέχοντα βήματα.", + "AIChatbotsContentPagesTitle": "Σελίδες", + "ContentUrl": "URL", + "AIChatbotsAIFavouredPagesDocumentation": "Εμφανίζει σελίδες που καλούνται συχνά από τα μποτ ΤΝ αλλά λιγότερο συχνά από ανθρώπους. Χρήσιμο για να ανακαλύπτετε περιεχόμενο που φέρνουν στην επιφάνεια ή αναφέρονται σε αυτό συστήματα ΤΝ που δεν έχουν ακόμη προσελκύσει ανθρώπινη αλληλεπίδραση.", + "AIChatbotsAIFavouredPagesTitle": "Προτιμώμενες από ΤΝ Σελίδες", + "AIChatbotsBrokenContentDocumentation": "Η αναφορά εμφανίζει λίστα προβληματικών διευθύνσεων σελίδων URL και διευθύνσεων URL εγγράφων (όπως αρχεία PDF, Word ή Excel) που προσπελάστηκαν από μποτ TN (όπως το ChatGPT) αλλά επέστρεψαν σφάλματα ή δεν ήταν διαθέσιμες. Βοηθά στην αναγνώριση ελλιπούς ή μη προσπελάσιμου περιεχόμενου που τα LLM δοκίμασαν να προσπελάσουν, επιτρέποντάς σας έτσι να διορθώσετε προβληματικούς συνδέσμους και να βελτιώσετε την αξιοπιστία και τον βαθμό εύρεσής τους.", + "AIChatbotsBrokenContentTitle": "Προβληματικές Σελίδες και Έγγραφα", + "AIChatbotsHumanFavouredPagesDocumentation": "Εμφανίζει σελίδες που δέχονται σημαντικά περισσότερες επισκέψεις από ανθρώπους παρά από μποτ ΤΝ. Χρήσιμο για τον εντοπισμό περιεχομένου που αντηχεί περισσότερο σε πραγματικούς χρήστες αλλά υποεκπροσωπεύεται σε κίνηση οδηγούμενη από ΤΝ ή σε προτάσεις.", + "AIChatbotsHumanFavouredPagesTitle": "Σελίδες Προτιμώμενες από Ανθρώπους", + "ColumnAIChatbotRequests": "Αιτήσεις από Μποτ ΤΝ", + "ColumnAIChatbotRequestsDocumentation": "Ο συνολικός αριθμός φορών που αυτή η διεύθυνση URL ζητήθηκε από μποτ ΤΝ κατά την επιλεγμένη περίοδο.", + "ColumnAvgResponseSize": "Μέσο Μέγεθος Απόκρισης", + "ColumnAvgResponseSizeDocumentation": "Μέσο μέγεθος των απαντήσεων που επέστρεψαν αυτές οι αιτήσεις. Αυτό βασίζεται σε ιχνηλάτηση του διακομιστή ή CDN και ενδέχεται να είναι κενό αν δεν παραμετροποιηθεί ή κενό για μια διεύθυνση URL όταν καμία από τις αιτήσεις δεν μεταφέρει τα δεδομένα αυτά.", + "ColumnAvgServerTime": "Μέσος Χρόνος Διακομιστή", + "ColumnAvgServerTimeDocumentation": "Μέσος χρόνος που ο διακομιστής χρειάστηκε για να απαντήσει σε αυτές τις αιτήσεις. Αυτό βασίζεται σε ιχνηλάτηση του διακομιστή ή CDN και ενδέχεται να είναι κενό αν δεν παραμετροποιηθεί ή κενό για μια διεύθυνση URL όταν καμία από τις αιτήσεις δεν μεταφέρει τα δεδομένα αυτά.", + "ColumnDiscrepancyScore": "Σκορ Ανακολουθίας", + "ColumnDiscrepancyScoreAIFavouredDocumentation": "Ένα σκορ από το 0 έως το 100 που συνδυάζει το πόσο αυτή η σελίδα κλίνει προς μποτ ΤΝ (πόσο συχνά ζητείται από μποτ ΤΝ σε σχέση με ανθρώπους) με το πόσο πολλή κίνηση από ΤΝ έχει σε σχέση με την πιο συχνά ζητούμενη σελίδα. Μεγαλύτερο σημαίνει ότι τα μποτ ΤΝ επικεντρώνονται σε αυτή πολύ περισσότερο σε σχέση με τους ανθρώπους. Οι σελίδες που είναι ισοζυγισμένες ή που κλίνουν οι άνθρωποι έχουν σκορ 0.", + "AIChatbotsContentDocumentsDocumentation": "Η αναφορά εμφανίζει ποιες διευθύνσεις URL ήταν προσπελάσιμες από μποτ ΤΝ (όπως το ChatGPT) για την λήψη πληροφοριών σε πραγματικό χρόνο κατά τη διάρκεια συζητήσεων χρηστών. Βοηθά στον εντοπισμό τμημάτων του ιστοτόπου σας που κάνουν αναφορά τα συστήματα ΤΝ, παρέχοντάς σας μια εικόνα σχετικά με την ορατότητα του ιστοτόπου από βοηθούμενες από ΤΝ αλληλεπιδράσεις και πιθανές ευκαιρίες περιεχομένου. Εστιάζει μόνο σε επισκέψεις για έγγραφα και αρχεία μέσων όπως PDF, Word, Excel και MP4, εξαιρώντας διευθύνσεις URL σελίδων HTML — που καλύπτονται σε ξεχωριστή αναφορά ειδικά για τις σελίδες.\nΕίναι σημαντικό να σημειωθεί ότι κανένα από αυτά τα έγγραφα δεν προβλήθηκε από ανθρώπους με τον παραδοσιακό τρόπο — όλες οι αιτήσεις προέρχονται από βοηθούς ΤΝ που τραβούν αυτόματα περιεχόμενο.\nΑυτή τη στιγμή, οι αναφορές αυτές περιλαμβάνουν αποκλειστικά αιτήσεις από μποτ ΤΝ που δεν εκτελούν JavaScript. Δεν περιλαμβάνουν κίνηση από εργαλεία συλλογής ΤΝ που χρησιμοποιούνται από μοντέλα ΤΝ ή από βοηθούς ΤΝ ικανούς να εκτελούν JavaScript.", + "AIChatbotsContentDocumentsTitle": "Έγγραφα", + "AIChatbotsContentPagesDocumentation": "Η αναφορά δείχνει ποιες διευθύνσεις URL σελίδων HTML προσπελάστηκαν από βοηθούς ΤΝ (όπως το ChatGPT) για την λήψη πληροφοριών σε πραγματικό χρόνο κατά τη διάρκεια συζητήσεων χρηστών. Σας βοηθά να εντοπίσετε ποια τμήματα του ιστοτόπου σας κάνουν αναφορά τα συστήματα ΤΝ, παρέχοντάς σας μια εικόνα για την ορατότητα του ιστοτόπου από υποβοηθούμενες από ΤΝ αλληλεπιδράσεις και πιθανές ευκαιρίες περιεχομένου. Εστιάζει μόνο σε επισκέψεις σε κοινές ιστοσελίδες, εξαιρώντας τις λήψεις αρχείων όπως εγγράφων PDF, Word, Excel — που καλύπτονται από ξεχωριστή αναφορά ειδικά για την πρόσβαση εγγράφων.\nΕίναι σημαντικό να σημειωθεί ότι αυτές οι σελίδες προβλήθηκαν από ανθρώπους με τον παραδοσιακό τρόπο — όλες οι αιτήσεις προέρχονται από βοηθούς ΤΝ κατά την αυτόματη λήψη περιεχομένου.\nΑυτή τη στιγμή, οι αναφορές αυτές περιλαμβάνουν αναφορές από μποτ ΤΝ που δεν εκτελούν JavaScript. Δεν περιλαμβάνουν κίνηση από εργαλεία συλλέκτες ΤΝ που χρησιμοποιούνται για την εκπαίδευση μοντέλων ΤΝ ή από βοηθούς ΤΝ ικανούς να εκτελούν JavaScript.", + "AIChatbotsContentRequests": "Αιτήσεις Περιεχομένου από Μποτ ΤΝ", + "AIChatbotsContentRequestsHelp1": "Η αναφορά δείχνει ποιες σελίδες και έγγραφα στον ιστοτόπο σας έχουν ζητηθεί από μποτ ΤΝ όπως το ChatGPT και μποτ βασισμένα σε παρόμοια μοντέλα large language. Οι αναφορές δείχνουν με λεπτομέρεια ποιες σελίδες HTML και αρχεία διαθέσιμα για λήψη (όπως έγγραφα PDF, Word ή Excel) προσπελάστηκαν, μαζί με αυτά που δεν ήταν δυνατή η λήψη τους λόγω σφαλμάτων. Αυτό σας βοηθά να κατανοήσετε σε ποιο περιεχόμενο κάνουν αναφορά τα συστήματα ΤΝ, να εντοπίσετε σπασμένους ή μη προσπελάσιμους πόροιυς και να βεβαιωθείτε ότι ο ιστοτόπος παρέχει αξιόπιστη πρόσβαση σε πληροφορία που χρησιμοποιείται από υποβοηθούμενα από ΤΝ εργαλεία.", + "AIChatbotsContentRequestsHelp2": "Είναι σημαντικό να σημειωθεί ότι καμία από αυτές τις σελίδες και κανένα από αυτά τα έγγραφα δεν προβλήθηκαν από ανθρώπους με τον παραδοσιακό τρόπο — όλες οι αιτήσεις προέρχονται από βοηθούς ΤΝ που τραβούν αυτόματα περιεχόμενο.", + "AIChatbotsContentRequestsHelp3": "Αυτή τη στιγμή, οι αναφορές αυτές περιλαμβάνουν μόνο αιτήσεις από μποτ ΤΝ που δεν εκτελούν JavaScript και εξαιρούν κίνηση από εργαλεία συλλέκτες ΤΝ που χρησιμοποιούνται για την εκπαίδευση μοντέλων ή βοηθών ΤΝ ικανούς να εκτελούν JavaScript.", + "ColumnDiscrepancyScoreHumanFavouredDocumentation": "Ένα σκορ από το 0 έως το 100 που συνδυάζει το πόσο μια σελίδα κλίνει περισσότερο προς ανθρώπους (πόσο περισσότερο προβάλλεται από ανθρώπους από όσο καλείται από μποτ ΤΝ) με το πόσο περισσότερη κίνηση από ανθρώπους έχει σε σχέση με την πιο συχνά επισκεπτόμενη σελίδα. Ψηλότερες τιμές σημαίνουν ότι οι άνθρωποι αλληλεπιδρούν με αυτήν πολύ περισσότερο από όσο τα μποτ ΤΝ. Οι σελίδες που είναι ισοζυγισμένες έχουν σκορ 0.", + "ColumnPageNotFound404Requests": "Αιτήσεις Σελίδων που Δεν Βρέθηκαν (404)", + "ColumnPageNotFound404RequestsDocumentation": "Αριθμός αιτήσεων μποτ που επέστρεψαν μια απόκριση HTTP 404 (Δε Βρέθηκε) ή 410 (Απών).", + "ColumnServerError5xxRequests": "Αιτήσεις 5ΧΧ", + "ColumnServerError5xxRequestsDocumentation": "Αριθμός αιτήσεων μποτ που επέστρεψαν μια αίτηση HTTP 5xx (σφάλμα διακομιστή).", + "ColumnTotalBrokenRequests": "Σύνολο Σπασμένων Αιτήσεων", + "ColumnTotalBrokenRequestsDocumentation": "Αριθμός αιτήσεων μποτ που επέστρεψαν μια απόκριση HTTP 404 (Δε Βρέθηκε), 410 (Απών) ή 5xx (Σφάλμα διακομιστή).", + "ColumnUniqueHumanPageviews": "Μοναδικές Προβολές από Ανθρώπους", + "ColumnUniqueHumanPageviewsDocumentation": "Ο αριθμός των επισκέψεων κατά τις οποίες πραγματικοί χρήστες είδαν το URL τουλάχιστον μία φορά. Η κίνηση από μποτ ΤΝ και άλλα μποτ εξαιρείται.", + "AIChatbotsLast30MinutesDocumentation": "Εμφανίζει τις αιτήσεις από μποτ ΤΝ που καταγράφηκαν τα τελευταία 30 λεπτά, ομαδοποιημένες ανά μποτ.", + "AIChatbotsLast30MinutesTitle": "Μποτ ΤΝ - Τελευταία 30 λεπτά", + "AIChatbotsLast8HoursDocumentation": "Εμφανίζει τις αιτήσεις από μποτ ΤΝ που καταγράφηκαν τις τελευταίες 8 ώρες, ομαδοποιημένες ανά μποτ.", + "AIChatbotsLast8HoursTitle": "Μποτ ΤΝ - Τελευταίες 8 ώρες", + "AIChatbotsRealtime": "Μποτ ΤΝ σε πραγματικό χρόνο", + "AIChatbotsRealtimeHelp1": "Η σελίδα Μποτ ΤΝ σε πραγματικό χρόνο εμφανίζει την πρόσφατη δραστηριότητα από ανεπεξέργαστα δεδομένα ιχνηλάτησης μποτ, συμπεριλαμβανομένου τον όγκο αιτήσεων από μποτ, μοναδικά URL σελίδων και αριθμό σφαλμάτων HTTP.", + "AIChatbotsRealtimeHelp2": "Αυτές οι αναφορές περιορίζονται σε σύντομα παράθυρα πραγματικού χρόνου για να παραμένουν γρήγορες και προβλέψιμες σε μεγάλους ιστοτόπους.", + "AIChatbotsRealTimeReportLimitFooter": "Αυτή η αναφορά περιορίζεται στις κορυφαίες %1$s γραμμές.", + "TopPageUrlsLast30MinutesDocumentation": "Εμφανίζει τις διευθύνσεις URL των σελίδων που ζητήθηκαν από μποτ ΤΝ στα τελευταία 30 λεπτά, ταξινομημένα με τις αιτήσεις των μποτ.", + "TopPageUrlsLast30MinutesTitle": "Κορυφαίες διευθύνσεις σελίδων URL - Τελευταία 30 λεπτά", + "TopPageUrlsLast8HoursDocumentation": "Εμφανίζει τις διευθύνσεις URL που ζητήθηκαν από τα μποτ ΤΝ τις τελευταίες 8 ώρες, ταξινομημένες με τις αιτήσεις των μποτ.", + "TopPageUrlsLast8HoursTitle": "Κορυφαίες διευθύνσεις URL σελίδων - Τελευταίες 8 ώρες" + } +} diff --git a/plugins/BotTracking/lang/en.json b/plugins/BotTracking/lang/en.json new file mode 100644 index 0000000000000000000000000000000000000000..266900e2fa6f30fe6950918b4fc783e499131d5f --- /dev/null +++ b/plugins/BotTracking/lang/en.json @@ -0,0 +1,113 @@ +{ + "BotTracking": { + "AIChatbotsAIFavouredPagesDocumentation": "Shows pages that are frequently requested by AI chatbots but less often visited by humans. Useful for discovering content that AI systems surface or reference but that doesn't yet attract much direct human engagement.", + "AIChatbotsAIFavouredPagesTitle": "AI-Favoured Pages", + "AIChatbotsBrokenContentDocumentation": "This report lists broken page URLs and document URLs (such as PDFs, Word, or Excel files) that were accessed by AI Chatbots (like ChatGPT) but returned errors or were unavailable. It helps identify missing or inaccessible content that LLMs attempted to fetch, allowing you to fix broken links and improve your site’s reliability and discoverability.", + "AIChatbotsBrokenContentTitle": "Broken Pages and Documents", + "AIChatbotsContentDocumentsDocumentation": "This report shows which document URLs were accessed by AI Chatbots (like ChatGPT) to retrieve real-time information during user chats. It helps you identify which parts of your website are being referenced by AI systems, offering insights into your site’s visibility in AI-assisted interactions and potential content opportunities. It focuses only on visits to documents and media files like PDF, Word, Excel and MP4, excluding HTML page URLs — which are covered in a separate report dedicated to pages.\nIt’s important to note that none of these documents were actually viewed by humans in the traditional way — all requests originate from AI assistants fetching content automatically.\nCurrently, these reports exclusively include requests from AI chatbots that do not execute JavaScript. They do not include traffic from AI crawlers used for training AI models or from AI agents capable of executing JavaScript.", + "AIChatbotsContentDocumentsTitle": "Documents", + "AIChatbotsContentPagesDocumentation": "This report shows which HTML page URLs were accessed by AI Assistants (like ChatGPT) to retrieve real-time information during user chats. It helps you identify which parts of your website are being referenced by AI systems, offering insights into your site’s visibility in AI-assisted interactions and potential content opportunities. It focuses only on visits to standard web pages, excluding file downloads such as PDFs, Word, or Excel documents — which are covered in a separate report dedicated to document access.\nIt’s important to note that none of these pages were actually viewed by humans in the traditional way — all requests originate from AI assistants fetching content automatically.\nCurrently, these reports exclusively include requests from AI bots that do not execute JavaScript. They do not include traffic from AI crawlers used for training AI models or from AI agents capable of executing JavaScript.", + "AIChatbotsContentPagesTitle": "Pages", + "AIChatbotsContentRequests": "AI Chatbots Content Requests", + "AIChatbotsContentRequestsHelp1": "This reporting page shows which pages and documents on your website have been requested by AI chatbots such as ChatGPT and similar large language model–based bots. These reports detail which HTML pages and downloadable files (like PDFs, Word, or Excel documents) were accessed, along with those that could not be retrieved due to errors. This helps you understand which content AI systems are referencing, identify broken or inaccessible resources, and ensure your website provides reliable access to information used by AI-driven tools.", + "AIChatbotsContentRequestsHelp2": "It’s important to note that none of these pages or documents were actually viewed by humans in the traditional way — all requests originate from AI assistants fetching content automatically.", + "AIChatbotsContentRequestsHelp3": "Currently, these reports only include requests from AI bots that do not execute JavaScript, and exclude traffic from AI crawlers used for model training or AI agents capable of running JavaScript.", + "AIChatbotsHumanFavouredPagesDocumentation": "Shows pages that receive significantly more visits from humans than from AI chatbots. Useful for identifying content that resonates strongly with real users but is underrepresented in AI-driven traffic or recommendations.", + "AIChatbotsHumanFavouredPagesTitle": "Human-Favoured Pages", + "AIChatbotsLast30MinutesDocumentation": "Shows AI chatbot requests recorded during the last 30 minutes, grouped by chatbot.", + "AIChatbotsLast30MinutesTitle": "AI Chatbots - Last 30 Minutes", + "AIChatbotsLast8HoursDocumentation": "Shows AI chatbot requests recorded during the last 8 hours, grouped by chatbot.", + "AIChatbotsLast8HoursTitle": "AI Chatbots - Last 8 Hours", + "AIChatbotsOverview": "AI Chatbots Overview", + "AIChatbotsOverviewHelp1": "The AI Chatbots Overview page provides insights into website traffic originating from AI chatbots such as ChatGPT and other large language model–based assistants. These reports track key metrics including the number of requests made by these bots, the pages and documents they access, and any errors encountered. They also offer detailed breakdowns showing which bots visit specific page URLs, helping you understand how AI chatbots interact with your content and identify opportunities to improve visibility and accessibility for AI-driven users.", + "AIChatbotsOverviewHelp2": "It’s important to note that none of these pages were actually viewed by humans in the traditional way — all requests originate from AI chatbots fetching content automatically.", + "AIChatbotsOverviewHelp3": "Currently, these reports exclusively include requests from AI chatbots that do not execute JavaScript. They do not include traffic from AI crawlers used for training AI models or from AI agents capable of executing JavaScript.", + "AIChatbotsRealtime": "AI Chatbots Real-time", + "AIChatbotsRealtimeHelp1": "The AI Chatbots Real-time page shows recent AI chatbot activity from raw bot tracking data, including chatbot request volume, unique page URLs, and HTTP error counts.", + "AIChatbotsRealtimeHelp2": "These reports are limited to short real-time windows so they remain fast and predictable on large sites.", + "AIChatbotsRealTimeReportLimitFooter": "This report is limited to the top %1$s rows.", + "AIChatbotsReportDocumentation": "Discover which AI chatbots are crawling your site and how many hits each one generates. Expand a chatbot to review the most frequently requested pages or documents.", + "AIChatbotsReportTitle": "AI Chatbots", + "ChatbotsOverTimeReportDocumentation": "This report charts the evolution of AI chatbot activity. Click any metric to enlarge its graph and review the underlying trend.", + "ColumnAIChatbotName": "AI Chatbot Name", + "ColumnAIChatbotRequests": "AI Chatbot Requests", + "ColumnAIChatbotRequestsDocumentation": "The total number of times this URL was requested by AI chatbots during the selected period.", + "ColumnAcquiredVisits": "Acquired visits", + "ColumnAcquiredVisitsDocumentation": "Visits that started after someone clicked from an AI chatbot (Referrers → AI Assistants).", + "ColumnAvgResponseSize": "Avg. Response Size", + "ColumnAvgResponseSizeDocumentation": "Average size of the responses returned for these requests. This relies on server or CDN tracking and may be empty if it is not configured, or empty for a URL when none of its requests carried this data.", + "ColumnAvgServerTime": "Avg. Server Time", + "ColumnAvgServerTimeDocumentation": "Average time the server took to respond to these requests. This relies on server or CDN tracking and may be empty if it is not configured, or empty for a URL when none of its requests carried this data.", + "ColumnClickThroughRate": "Click-through rate", + "ColumnClickThroughRateDocumentation": "Share of AI chatbot requests that resulted in acquired visits (acquired visits ÷ requests).", + "ColumnDiscrepancyScore": "Discrepancy Score", + "ColumnDiscrepancyScoreAIFavouredDocumentation": "A score from 0 to 100 that combines how strongly this page leans towards AI chatbots (how much more it is requested by chatbots than visited by humans) with how much chatbot traffic it has relative to the most-requested page. Higher means AI chatbots focus on it far more than humans do; pages that are balanced or human-leaning score 0.", + "ColumnDiscrepancyScoreHumanFavouredDocumentation": "A score from 0 to 100 that combines how strongly this page leans towards humans (how much more it is visited by humans than requested by AI chatbots) with how much human traffic it has relative to the most-visited page. Higher means humans engage with it far more than AI chatbots do; pages that are balanced or AI-leaning score 0.", + "ColumnDocumentRequests": "Document Requests", + "ColumnDocumentRequestsDocumentation": "Total number of bot requests to document URLs recorded during the selected period.", + "ColumnNotFoundRequests": "Not found requests", + "ColumnNotFoundRequestsDocumentation": "Number of bot requests that returned an HTTP 404 or 410 response, including document URLs.", + "ColumnPageNotFound404Requests": "Page Not Found (404) Requests", + "ColumnPageNotFound404RequestsDocumentation": "Number of bot requests that returned an HTTP 404 (Not Found) or 410 (Gone) response.", + "ColumnPageRequests": "Page Requests", + "ColumnPageRequestsDocumentation": "Total number of bot requests to page URLs recorded during the selected period.", + "ColumnRequests": "Requests", + "ColumnRequestsDocumentation": "Total number of bot requests recorded during the selected period. Includes both page and document URLs.", + "ColumnServerError5xxRequests": "5XX Requests", + "ColumnServerError5xxRequestsDocumentation": "Number of bot requests that returned an HTTP 5xx (server error) response.", + "ColumnServerErrorRequests": "Server error (5xx) requests", + "ColumnServerErrorRequestsDocumentation": "Number of bot requests that returned an HTTP 5xx response, including document URLs.", + "ColumnTotalBrokenRequests": "Total Broken Requests", + "ColumnTotalBrokenRequestsDocumentation": "Number of bot requests that returned an HTTP 404 (Not Found), 410 (Gone), or 5xx (server error) response.", + "ColumnUniqueAiChatbots": "Unique AI chatbots", + "ColumnUniqueAiChatbotsDocumentation": "AI chatbot user agents detected during the selected period.", + "ColumnUniqueDocumentUrls": "Unique document URLs", + "ColumnUniqueDocumentUrlsDocumentation": "Distinct download or document URLs requested by AI chatbots.", + "ColumnUniqueHumanPageviews": "Unique Human Pageviews", + "ColumnUniqueHumanPageviewsDocumentation": "The number of visits during which real users viewed this URL at least once. Traffic from AI chatbots and other bots is excluded.", + "ColumnUniquePageUrls": "Unique page URLs", + "ColumnUniquePageUrlsDocumentation": "Distinct page URLs requested by AI chatbots.", + "ContentUrl": "URL", + "DetectingYourSite": "Detecting your site…", + "DocumentUrl": "Document URL", + "NoDataAmazonCloudFrontInstructions1": "This integration uses an AWS Lambda that reads Amazon CloudFront access logs from S3 and sends Matomo hits in batches. The tracker code is maintained externally, so follow the repository README for the current setup and deployment details.", + "NoDataAmazonCloudFrontInstructions2": "Deploy the Lambda (Node.js) and set your Matomo URL and site ID.", + "NoDataAmazonCloudFrontInstructions3": "Attach an S3 trigger for Amazon CloudFront log objects (for example, ObjectCreated on .gz logs).", + "NoDataAmazonCloudFrontInstructions4": "Verify the setup by checking that AI Chatbot hits are showing in tracking reports.", + "NoDataCloudflareInstructions1": "This integration uses a Cloudflare Worker that proxies requests to your origin and asynchronously posts Matomo hits without delaying responses. The tracker code is maintained externally, so follow the repository README for the current setup and deployment details.", + "NoDataCloudflareInstructions2": "Deploy the Worker (Node.js) and set your Matomo URL and site ID.", + "NoDataCloudflareInstructions3": "Attach the Worker to your zone or route so it runs inline with requests.", + "NoDataCloudflareInstructions4": "Verify the setup by checking that AI Chatbot hits are showing in tracking reports.", + "NoDataHttpTrackingApi": "HTTP Tracking API", + "NoDataHttpTrackingApiDescription": "The HTTP Tracking API lets you track AI chatbot requests directly when you need a custom integration. Start with the %1$sTracking API knowledge base%2$s and use the %3$sHTTP Tracking API reference%4$s for the latest bot tracking parameters and behaviors.", + "NoDataLinkKnowledgeBase": "Documentation: %1$sMatomo Knowledge Base%2$s", + "NoDataLinkSourceCode": "Source code: %1$sGitHub repository%2$s", + "NoDataMissingOneContactUs": "Missing one? Contact us", + "NoDataWordPressInstructions1": "The WordPress integration adds a small bridge that sends AI Chatbot signals to Matomo. Because the tracker is maintained externally, the README is the source of truth for current steps.", + "NoDataWordPressInstructions2": "Install and activate the plugin.", + "NoDataWordPressInstructions3": "Enter your Matomo URL and site ID.", + "NoDataWordPressInstructions4": "Verify the setup by checking that AI Chatbot hits are showing in tracking reports.", + "NoRecentAIBotRequests": "%1$sNo data collected in the last 7 days.%2$s %3$sSet up AI Chatbot tracking now%4$s.", + "NoRecentRequestsWidgetTitle": "AI Chatbots - No Recent Requests", + "PageUrl": "Page URL", + "PluginDescription": "Track and analyze visits from AI chatbots such as ChatGPT-User and similar. This plugin helps you understand how AI-driven tools interact with your website by logging and reporting bot-triggered requests.", + "ReportTitleChatbotsOverTime": "AI Chatbots Over Time", + "ReportTitleChatbotsOverview": "AI Chatbots Overview", + "SegmentNotSupported": "Report does not support segmentation. The data displayed is your standard, unsegmented report data.", + "SiteWithoutDataBackToMatomo": "Back to Matomo", + "SiteWithoutDataChooseTrackingMethod": "Choose your AI Chatbot Tracking Method", + "SiteWithoutDataChooseTrackingMethodPreamble1": "Track AI chatbots accessing your website to understand how your content is discovered and consumed by AI-powered assistants.", + "SiteWithoutDataChooseTrackingMethodPreamble2": "AI chatbots typically do not execute JavaScript. As a result, they are not tracked by default JavaScript-based analytics, and additional tracking configuration is required.", + "SiteWithoutDataInstallWithX": "Install with %1$s", + "SiteWithoutDataInstallWithXRecommendation": "Install AI Chatbots tracking with %1$s (recommended for you)", + "SiteWithoutDataNotYetReady": "Not ready to set up AI Chatbot Tracking just yet?", + "SiteWithoutDataOtherInstallMethods": "Other methods to track AI Chatbots", + "SiteWithoutDataOtherInstallMethodsIntro": "We offer multiple tracking paths - select the one that matches your setup.", + "SiteWithoutDataRecommendationText": "We have detected that your website is using %1$s, so you can set up AI Chatbots tracking using our official %1$s integration.", + "SiteWithoutDataStepByStepGuide": "Step-by-step guide", + "TopPageUrlsLast30MinutesDocumentation": "Shows page URLs requested by AI chatbots during the last 30 minutes, ordered by chatbot requests.", + "TopPageUrlsLast30MinutesTitle": "Top Page URLs - Last 30 minutes", + "TopPageUrlsLast8HoursDocumentation": "Shows page URLs requested by AI chatbots during the last 8 hours, ordered by chatbot requests.", + "TopPageUrlsLast8HoursTitle": "Top Page URLs - Last 8 hours" + } +} diff --git a/plugins/BotTracking/lang/eo.json b/plugins/BotTracking/lang/eo.json new file mode 100644 index 0000000000000000000000000000000000000000..6f1b534c556b6a96d832d220fe1b61ceb6fd3cfe --- /dev/null +++ b/plugins/BotTracking/lang/eo.json @@ -0,0 +1,6 @@ +{ + "BotTracking": { + "SiteWithoutDataBackToMatomo": "Reen al matomo", + "AIChatbotsContentPagesTitle": "Paĝoj" + } +} diff --git a/plugins/BotTracking/lang/es-ar.json b/plugins/BotTracking/lang/es-ar.json new file mode 100644 index 0000000000000000000000000000000000000000..9e5ad3df40a8a56bf64937652b6e183c92e97504 --- /dev/null +++ b/plugins/BotTracking/lang/es-ar.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "Dirección web de la página", + "NoDataHttpTrackingApi": "API de rastreo HTTP", + "SiteWithoutDataBackToMatomo": "Volver a Matomo", + "AIChatbotsContentPagesTitle": "Páginas", + "ContentUrl": "Dirección web" + } +} diff --git a/plugins/BotTracking/lang/es.json b/plugins/BotTracking/lang/es.json new file mode 100644 index 0000000000000000000000000000000000000000..948498442983314520d5cfd2b56898c1817d2963 --- /dev/null +++ b/plugins/BotTracking/lang/es.json @@ -0,0 +1,11 @@ +{ + "BotTracking": { + "PageUrl": "URL de la página", + "NoDataHttpTrackingApi": "API de seguimiento HTTP", + "SiteWithoutDataBackToMatomo": "Volver a Matomo", + "SiteWithoutDataInstallWithX": "Instalar con %1$s", + "SiteWithoutDataStepByStepGuide": "Guía paso a paso", + "AIChatbotsContentPagesTitle": "Paginas", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/et.json b/plugins/BotTracking/lang/et.json new file mode 100644 index 0000000000000000000000000000000000000000..33257b9ce611952bc749345d5033ada84e7df9f3 --- /dev/null +++ b/plugins/BotTracking/lang/et.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "Lehe URL", + "SiteWithoutDataBackToMatomo": "Tagasi Matomosse", + "AIChatbotsContentPagesTitle": "Leheküljed", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/eu.json b/plugins/BotTracking/lang/eu.json new file mode 100644 index 0000000000000000000000000000000000000000..7d99ab3081f824611d08d0ce5ff703c5486e7ca6 --- /dev/null +++ b/plugins/BotTracking/lang/eu.json @@ -0,0 +1,11 @@ +{ + "BotTracking": { + "PageUrl": "Orriaren URLa", + "NoDataHttpTrackingApi": "HTTP jarraipen APIa", + "SiteWithoutDataBackToMatomo": "Itzuli Matomora", + "SiteWithoutDataInstallWithX": "Instalatu honekin: %1$s", + "SiteWithoutDataStepByStepGuide": "Pausoz pausoko gida", + "AIChatbotsContentPagesTitle": "Orriak", + "ContentUrl": "URLa" + } +} diff --git a/plugins/BotTracking/lang/fa.json b/plugins/BotTracking/lang/fa.json new file mode 100644 index 0000000000000000000000000000000000000000..8547a136984b94d8a43519539a08605aa6cc337d --- /dev/null +++ b/plugins/BotTracking/lang/fa.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "آدرس صفحه", + "NoDataHttpTrackingApi": "API ردیابی HTTP", + "SiteWithoutDataBackToMatomo": "بازگشت به Matomo", + "AIChatbotsContentPagesTitle": "صفحه‌ها", + "ContentUrl": "آدرس" + } +} diff --git a/plugins/BotTracking/lang/fi.json b/plugins/BotTracking/lang/fi.json new file mode 100644 index 0000000000000000000000000000000000000000..b96fcb7ceba9bfb0377445365c9b67ccfbf0597d --- /dev/null +++ b/plugins/BotTracking/lang/fi.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "Sivun URL", + "NoDataHttpTrackingApi": "HTTP-seurantarajapinta", + "SiteWithoutDataBackToMatomo": "Takaisin Matomoon", + "AIChatbotsContentPagesTitle": "Sivut", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/fr.json b/plugins/BotTracking/lang/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..ddabd6c937f0413970191bf675d8ae5d7ac61f10 --- /dev/null +++ b/plugins/BotTracking/lang/fr.json @@ -0,0 +1,71 @@ +{ + "BotTracking": { + "PageUrl": "URL de la page", + "NoDataHttpTrackingApi": "API de suivi HTTP", + "SiteWithoutDataBackToMatomo": "Retourner à Matomo", + "SiteWithoutDataInstallWithX": "Installer avec %1$s", + "SiteWithoutDataStepByStepGuide": "Guide étape par étape", + "AIChatbotsOverview": "Vue d'ensemble des chatbots IA", + "AIChatbotsOverviewHelp1": "La page Vue d'ensemble des chatbots IA fournit des informations sur le trafic du site web provenant des chatbots IA tels que ChatGPT et d'autres assistants basés sur des grands modèles de langage. Ces rapports suivent des métriques clés, notamment le nombre de requêtes effectuées par ces robots, les pages et documents auxquels ils accèdent, ainsi que les erreurs rencontrées. Ils offrent également des ventilations détaillées montrant quels robots visitent des URLs de pages spécifiques, vous aidant à comprendre comment les chatbots IA interagissent avec votre contenu et à identifier des opportunités pour améliorer la visibilité et l'accessibilité auprès des utilisateurs pilotés par l'IA.", + "AIChatbotsOverviewHelp2": "Il est important de noter qu'aucune de ces pages n'a été réellement consultée par des humains de manière traditionnelle — toutes les requêtes proviennent de chatbots IA récupérant le contenu automatiquement.", + "AIChatbotsOverviewHelp3": "Actuellement, ces rapports incluent exclusivement les requêtes des chatbots IA qui n'exécutent pas JavaScript. Ils n'incluent pas le trafic provenant des robots d'exploration IA utilisés pour l'entraînement de modèles IA ni des agents IA capables d'exécuter JavaScript.", + "AIChatbotsReportDocumentation": "Découvrez quels chatbots IA explorent votre site et combien de requêtes chacun génère. Développez un chatbot pour consulter les pages ou documents les plus fréquemment demandés.", + "AIChatbotsReportTitle": "Chatbots AI", + "ChatbotsOverTimeReportDocumentation": "Ce rapport représente l'évolution de l'activité des chatbots IA. Cliquez sur une métrique pour agrandir son graphique et examiner la tendance sous-jacente.", + "ColumnAIChatbotName": "Nom du Chatbot IA", + "ColumnAcquiredVisits": "Visites acquises", + "PluginDescription": "Suivez et analysez les visites des chatbots IA tels que ChatGPT-User et similaires. Ce plugin vous aide à comprendre comment les outils basés sur l'IA interagissent avec votre site web en enregistrant et en rapportant les requêtes déclenchées par les bots.", + "ColumnAcquiredVisitsDocumentation": "Visites démarrées après qu'un utilisateur a cliqué depuis un chatbot IA (Référents → Assistants IA).", + "ColumnDocumentRequests": "Requêtes de documents", + "ColumnDocumentRequestsDocumentation": "Nombre total de requêtes de bots vers des URL de documents enregistrées durant la période sélectionnée.", + "ColumnPageRequests": "Requêtes de pages", + "ColumnPageRequestsDocumentation": "Nombre total de requêtes de bots vers des URL de pages enregistrées durant la période sélectionnée.", + "ColumnRequests": "Requêtes", + "ColumnRequestsDocumentation": "Nombre total de requêtes de bots enregistrées durant la période sélectionnée. Inclut les URL de pages et de documents.", + "ColumnUniquePageUrls": "URL de pages uniques", + "ColumnUniquePageUrlsDocumentation": "URL de pages distinctes demandées par les chatbots IA.", + "ColumnUniqueDocumentUrls": "URL de documents uniques", + "ColumnUniqueDocumentUrlsDocumentation": "URL de téléchargement ou de documents distincts demandées par les chatbots IA.", + "ColumnUniqueAiChatbots": "Chatbots IA uniques", + "ColumnUniqueAiChatbotsDocumentation": "Agents utilisateurs de chatbots IA détectés durant la période sélectionnée.", + "ColumnNotFoundRequests": "Requêtes non trouvées", + "ColumnNotFoundRequestsDocumentation": "Nombre de requêtes de bots ayant retourné une réponse HTTP 404 ou 410, y compris les URL de documents.", + "ColumnServerErrorRequests": "Requêtes d'erreur serveur (5xx)", + "ColumnServerErrorRequestsDocumentation": "Nombre de requêtes de bots ayant retourné une réponse HTTP 5xx, y compris les URL de documents.", + "ColumnClickThroughRate": "Taux de clic", + "ColumnClickThroughRateDocumentation": "Part des requêtes de chatbots IA ayant conduit à des visites acquises (visites acquises ÷ requêtes).", + "DocumentUrl": "URL de document", + "SegmentNotSupported": "Le rapport ne prend pas en charge la segmentation. Les données affichées sont vos données de rapport standard, non segmentées.", + "ReportTitleChatbotsOverTime": "Chatbots IA dans le temps", + "ReportTitleChatbotsOverview": "Vue d'ensemble des chatbots IA", + "DetectingYourSite": "Détection de votre site…", + "NoDataAmazonCloudFrontInstructions1": "Cette intégration utilise une fonction AWS Lambda qui lit les journaux d'accès Amazon CloudFront depuis S3 et envoie des hits Matomo par lots. Le code du tracker est maintenu en externe, consultez le README du dépôt pour les détails de configuration et de déploiement actuels.", + "NoDataAmazonCloudFrontInstructions2": "Déployez la Lambda (Node.js) et définissez votre URL Matomo et l'ID de site.", + "NoDataAmazonCloudFrontInstructions3": "Attachez un déclencheur S3 pour les objets journaux Amazon CloudFront (par exemple, ObjectCreated sur les journaux .gz).", + "NoDataAmazonCloudFrontInstructions4": "Vérifiez la configuration en contrôlant que les hits des chatbots IA apparaissent dans les rapports de suivi.", + "NoDataCloudflareInstructions1": "Cette intégration utilise un Worker Cloudflare qui achemine les requêtes vers votre origine et publie des hits Matomo de manière asynchrone sans retarder les réponses. Le code du tracker est maintenu en externe, consultez le README du dépôt pour les détails de configuration et de déploiement actuels.", + "NoDataCloudflareInstructions2": "Déployez le Worker (Node.js) et définissez votre URL Matomo et l'ID de site.", + "NoDataCloudflareInstructions3": "Attachez le Worker à votre zone ou route pour qu'il s'exécute en ligne avec les requêtes.", + "NoDataCloudflareInstructions4": "Vérifiez la configuration en contrôlant que les hits des chatbots IA apparaissent dans les rapports de suivi.", + "NoDataHttpTrackingApiDescription": "L'API de suivi HTTP vous permet de suivre les requêtes des chatbots IA directement pour une intégration personnalisée. Consultez la %1$sbase de connaissances de l'API de suivi%2$s et la %3$sréférence de l'API de suivi HTTP%4$s pour les derniers paramètres et comportements de suivi des bots.", + "NoDataLinkKnowledgeBase": "Documentation : %1$sBase de connaissances Matomo%2$s", + "NoDataLinkSourceCode": "Code source : %1$sDépôt GitHub%2$s", + "NoDataMissingOneContactUs": "Il en manque un ? Contactez-nous", + "NoDataWordPressInstructions1": "L'intégration WordPress ajoute un petit pont qui envoie les signaux des chatbots IA à Matomo. Le tracker étant maintenu en externe, le README est la source de référence pour les étapes actuelles.", + "NoDataWordPressInstructions2": "Installez et activez le plugin.", + "NoDataWordPressInstructions3": "Entrez votre URL Matomo et l'ID de site.", + "NoDataWordPressInstructions4": "Vérifiez la configuration en contrôlant que les hits des chatbots IA apparaissent dans les rapports de suivi.", + "NoRecentAIBotRequests": "%1$sAucune donnée collectée au cours des 7 derniers jours.%2$s %3$sConfigurer le suivi des chatbots IA maintenant%4$s.", + "NoRecentRequestsWidgetTitle": "Chatbots IA - Aucune requête récente", + "SiteWithoutDataChooseTrackingMethod": "Choisissez votre méthode de suivi des chatbots IA", + "SiteWithoutDataChooseTrackingMethodPreamble1": "Suivez les chatbots IA accédant à votre site web pour comprendre comment votre contenu est découvert et consommé par les assistants propulsés par l'IA.", + "SiteWithoutDataChooseTrackingMethodPreamble2": "Les chatbots IA n'exécutent généralement pas JavaScript. Par conséquent, ils ne sont pas suivis par défaut par les analyses basées sur JavaScript, et une configuration de suivi supplémentaire est requise.", + "SiteWithoutDataInstallWithXRecommendation": "Installer le suivi des chatbots IA avec %1$s (recommandé pour vous)", + "SiteWithoutDataNotYetReady": "Pas encore prêt à configurer le suivi des chatbots IA ?", + "SiteWithoutDataOtherInstallMethods": "Autres méthodes pour suivre les chatbots IA", + "SiteWithoutDataOtherInstallMethodsIntro": "Nous proposons plusieurs options de suivi - sélectionnez celle qui correspond à votre configuration.", + "SiteWithoutDataRecommendationText": "Nous avons détecté que votre site web utilise %1$s, vous pouvez donc configurer le suivi des chatbots IA avec notre intégration officielle %1$s.", + "AIChatbotsContentPagesTitle": "Pages", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/ga.json b/plugins/BotTracking/lang/ga.json new file mode 100644 index 0000000000000000000000000000000000000000..abee1064826c1d0ab4db7cbfef5f41b3dd734e68 --- /dev/null +++ b/plugins/BotTracking/lang/ga.json @@ -0,0 +1,113 @@ +{ + "BotTracking": { + "PluginDescription": "Rianaigh agus déan anailís ar chuairteanna ó chomhrábotanna AI ar nós ChatGPT-User agus a leithéidí. Cabhraíonn an breiseán seo leat tuiscint a fháil ar an gcaoi a n-idirghníomhaíonn uirlisí atá á dtiomáint ag AI le do shuíomh Gréasáin trí iarratais spreagtha ag bot a logáil agus a thuairisciú.", + "PageUrl": "URL an leathanaigh", + "ColumnAcquiredVisits": "Cuairteanna faighte", + "ColumnAcquiredVisitsDocumentation": "Cuairteanna a thosaigh tar éis do dhuine cliceáil ó chomhrábot AI (Tagairtí → Cúntóirí AI).", + "ColumnDocumentRequests": "Iarratais ar Dhoiciméid", + "ColumnDocumentRequestsDocumentation": "Líon iomlán na n-iarratas bot chun URLanna a dhoiciméadú a taifeadadh le linn na tréimhse roghnaithe.", + "ColumnPageRequests": "Iarratais Leathanaigh", + "ColumnPageRequestsDocumentation": "Líon iomlán na n-iarratas bot chuig URLanna leathanaigh a taifeadadh le linn na tréimhse roghnaithe.", + "ColumnRequests": "Iarratais", + "ColumnRequestsDocumentation": "Líon iomlán na n-iarratas bot a taifeadadh le linn na tréimhse roghnaithe. Áirítear URLanna leathanaigh agus doiciméad araon.", + "ColumnUniquePageUrls": "URLanna leathanaigh uathúla", + "ColumnUniquePageUrlsDocumentation": "URLanna leathanaigh ar leith arna n-iarraidh ag comhrábotanna AI.", + "ColumnUniqueDocumentUrls": "URLanna doiciméad uathúla", + "ColumnUniqueDocumentUrlsDocumentation": "URLanna íoslódála nó doiciméad ar leith arna n-iarraidh ag comhrábotanna AI.", + "ColumnUniqueAiChatbots": "Comhrábotanna AI uathúla", + "ColumnUniqueAiChatbotsDocumentation": "Gníomhairí úsáideora comhrábot AI braite le linn na tréimhse roghnaithe.", + "ColumnNotFoundRequests": "Iarratais gan aimsiú", + "ColumnNotFoundRequestsDocumentation": "Líon na n-iarratas bot a thug freagra HTTP 404 nó 410 ar ais, lena n-áirítear URLanna doiciméad.", + "ColumnServerErrorRequests": "Iarratais earráide freastalaí (5xx)", + "ColumnServerErrorRequestsDocumentation": "Líon na n-iarratas bot a thug freagra HTTP 5xx ar ais, lena n-áirítear URLanna doiciméad.", + "ColumnClickThroughRate": "Ráta cliceáil tríd", + "ColumnClickThroughRateDocumentation": "Sciar d'iarratais comhrábot AI a raibh cuairteanna faighte mar thoradh orthu (cuairteanna faighte ÷ iarratais).", + "DocumentUrl": "URL an Doiciméid", + "SegmentNotSupported": "Ní thacaíonn an tuarascáil le deighilt. Is iad na sonraí a thaispeántar ná do shonraí tuarascála caighdeánacha, neamh-deighilte.", + "NoDataHttpTrackingApi": "API Rianaithe HTTP", + "SiteWithoutDataBackToMatomo": "Ar ais go Matomo", + "SiteWithoutDataInstallWithX": "Suiteáil le%1$s", + "SiteWithoutDataStepByStepGuide": "Treoir céim ar chéim", + "AIChatbotsOverview": "Forbhreathnú ar Chomhrábotanna Intleachta Saorga", + "AIChatbotsOverviewHelp1": "Tugann leathanach Forbhreathnú ar Chomhrábotanna AI léargas ar thrácht gréasáin a thagann ó chomhrábotanna AI ar nós ChatGPT agus cúntóirí móra teanga eile atá bunaithe ar mhúnlaí teanga. Rianaíonn na tuarascálacha seo príomh-mhéadrachtaí lena n-áirítear líon na n-iarratas a dhéanann na róbónna seo, na leathanaigh agus na doiciméid a bhfaigheann siad rochtain orthu, agus aon earráidí a bhíonn ann. Cuireann siad miondealú mionsonraithe ar fáil freisin a thaispeánann cé na róbónna a thugann cuairt ar URLanna leathanaigh shonracha, rud a chabhraíonn leat tuiscint a fháil ar an gcaoi a n-idirghníomhaíonn comhrábotanna AI le d’ábhar agus deiseanna a aithint chun infheictheacht agus inrochtaineacht a fheabhsú d’úsáideoirí atá á dtiomáint ag AI.", + "AIChatbotsOverviewHelp2": "Tá sé tábhachtach a thabhairt faoi deara nár fhéach daoine ar aon cheann de na leathanaigh seo ar an mbealach traidisiúnta — eascraíonn na hiarratais uile ó chomhrábots AI a bhailíonn ábhar go huathoibríoch.", + "AIChatbotsOverviewHelp3": "Faoi láthair, ní chuimsíonn na tuarascálacha seo ach iarratais ó chomhrábotanna AI nach ndéanann JavaScript a fhorghníomhú. Ní chuimsíonn siad trácht ó chraoltóirí AI a úsáidtear chun samhlacha AI a oiliúint ná ó ghníomhairí AI atá in ann JavaScript a fhorghníomhú.", + "AIChatbotsReportDocumentation": "Faigh amach cé na comhrábotanna AI atá ag crawláil do shuíomh agus cé mhéad amas a ghineann gach ceann acu. Leathnaigh comhrábot chun athbhreithniú a dhéanamh ar na leathanaigh nó na doiciméid is minice a iarrtar.", + "AIChatbotsReportTitle": "Comhrábotanna AI", + "ChatbotsOverTimeReportDocumentation": "Déanann an tuarascáil seo cairt de fhorbairt ghníomhaíocht chatbot AI. Cliceáil ar aon mhéadrach chun a ghraf a mhéadú agus an treocht bhunúsach a athbhreithniú.", + "ColumnAIChatbotName": "Ainm Chomhrábot AI", + "DetectingYourSite": "Ag braith do shuíomh…", + "NoDataAmazonCloudFrontInstructions1": "Úsáideann an comhtháthú seo AWS Lambda a léann logaí rochtana Amazon CloudFront ó S3 agus a sheolann amas Matomo i mbaisceanna. Coinnítear an cód rianaithe go seachtrach, mar sin lean README an stórais le haghaidh sonraí reatha an tsocraithe agus an imscartha.", + "NoDataAmazonCloudFrontInstructions2": "Imscaradh an Lambda (Node.js) agus socraigh do URL Matomo agus d'ID suímh.", + "NoDataAmazonCloudFrontInstructions3": "Ceangail spreagthóir S3 le haghaidh réada loga Amazon CloudFront (mar shampla, ObjectCreated ar logaí .gz).", + "NoDataAmazonCloudFrontInstructions4": "Fíoraigh an socrú trí sheiceáil go bhfuil buillí AI Chatbot le feiceáil i dtuarascálacha rianaithe.", + "NoDataCloudflareInstructions1": "Úsáideann an comhtháthú seo Oibrí Cloudflare a sheolann iarratais chuig do bhunadh agus a phostálann amas Matomo go haisioncrónach gan moill a chur ar fhreagraí. Coinnítear an cód rianaithe go seachtrach, mar sin lean README an stórais le haghaidh sonraí reatha an tsocraithe agus an imscartha.", + "NoDataCloudflareInstructions2": "Imscaradh an Oibrí (Node.js) agus socraigh do URL Matomo agus d'ID suímh.", + "NoDataCloudflareInstructions3": "Ceangail an Oibrí le do chrios nó do bhealach ionas go rithfidh sé i gcomhréir le hiarratais.", + "NoDataCloudflareInstructions4": "Fíoraigh an socrú trí sheiceáil go bhfuil buillí AI Chatbot le feiceáil i dtuarascálacha rianaithe.", + "NoDataHttpTrackingApiDescription": "Ligeann an HTTP Tracking API duit iarratais ar chomhrábot AI a rianú go díreach nuair a bhíonn comhtháthú saincheaptha de dhíth ort. Tosaigh leis an mbunachar eolais %1$sTracking API%2$s agus bain úsáid as an tagairt %3$sHTTP Tracking API%4$s chun na paraiméadair agus na hiompraíochtaí is déanaí maidir le rianú bot a fháil.", + "NoDataLinkKnowledgeBase": "Doiciméadú: %1$sBunachar Eolais Matomo%2$s", + "NoDataLinkSourceCode": "Cód foinse: %1$sGitHub stórlann%2$s", + "NoDataMissingOneContactUs": "Ceann ar iarraidh? Déan teagmháil linn", + "NoDataWordPressInstructions1": "Cuireann comhtháthú WordPress droichead beag leis a sheolann comharthaí AI Chatbot chuig Matomo. Ós rud é go gcothabháiltear an rianaitheoir go seachtrach, is é an README foinse na fírinne do na céimeanna reatha.", + "NoDataWordPressInstructions2": "Suiteáil agus gníomhachtaigh an breiseán.", + "NoDataWordPressInstructions3": "Cuir isteach do URL Matomo agus d’aitheantas suímh.", + "NoDataWordPressInstructions4": "Fíoraigh an socrú trí sheiceáil go bhfuil buillí AI Chatbot le feiceáil i dtuarascálacha rianaithe.", + "NoRecentAIBotRequests": "%1$sNíor bailíodh aon sonraí le 7 lá anuas.%2$s %3$sSocraigh rianú comhrábot AI anois%4$s.", + "NoRecentRequestsWidgetTitle": "Comhrábotanna AI - Gan Iarratais Le Déanaí", + "ReportTitleChatbotsOverTime": "Comhrábots AI le himeacht ama", + "ReportTitleChatbotsOverview": "Forbhreathnú ar Chomhrábotanna Intleachta Saorga", + "SiteWithoutDataChooseTrackingMethod": "Roghnaigh do Mhodh Rianaithe Comhrábot AI", + "SiteWithoutDataChooseTrackingMethodPreamble1": "Rianaigh comhrábots AI a théann chuig do shuíomh Gréasáin chun tuiscint a fháil ar an gcaoi a bhfaigheann agus a ídíonn cúntóirí faoi thiomáint AI d’ábhar.", + "SiteWithoutDataChooseTrackingMethodPreamble2": "De ghnáth ní dhéanann comhrábotanna AI JavaScript a fhorghníomhú. Mar thoradh air sin, ní dhéantar iad a rianú de réir anailísíochta réamhshocraithe atá bunaithe ar JavaScript, agus tá gá le cumraíocht rianaithe bhreise.", + "SiteWithoutDataInstallWithXRecommendation": "Suiteáil rianú comhrábot AI le %1$s (molta duit)", + "SiteWithoutDataNotYetReady": "Nach bhfuil tú réidh le Rianú Comhrábot AI a shocrú go fóill?", + "SiteWithoutDataOtherInstallMethods": "Modhanna eile chun comhrábots AI a rianú", + "SiteWithoutDataOtherInstallMethodsIntro": "Cuirimid cosáin rianaithe iolracha ar fáil - roghnaigh an ceann a oireann le do shocrú.", + "SiteWithoutDataRecommendationText": "Tá braite againn go bhfuil %1$s á úsáid ag do shuíomh Gréasáin, mar sin is féidir leat rianú comhrábots AI a shocrú ag baint úsáide as ár gcomhtháthú oifigiúil %1$s.", + "AIChatbotsContentPagesTitle": "Leathanaigh", + "ContentUrl": "URL", + "AIChatbotsBrokenContentDocumentation": "Liostaítear sa tuarascáil seo URLanna leathanaigh briste agus URLanna doiciméad (amhail comhaid PDF, Word, nó Excel) a ndearnadh rochtain orthu ag Comhrábotanna AI (cosúil le ChatGPT) ach a thug earráidí ar ais nó nach raibh ar fáil. Cuidíonn sé le hábhar atá ar iarraidh nó nach bhfuil inrochtana a aithint a rinne LLManna iarracht a fháil, rud a ligeann duit naisc briste a shocrú agus iontaofacht agus infhaighteacht do shuíomh a fheabhsú.", + "AIChatbotsBrokenContentTitle": "Leathanaigh agus Doiciméid Briste", + "AIChatbotsContentDocumentsDocumentation": "Taispeánann an tuarascáil seo cé na URLanna doiciméad a ndearna Comhrábotanna AI (cosúil le ChatGPT) rochtain orthu chun faisnéis fhíor-ama a aisghabháil le linn comhráite úsáideoirí. Cabhraíonn sé leat a aithint cé na codanna de do shuíomh Gréasáin a bhfuil córais AI ag tagairt dóibh, ag tabhairt léargais ar infheictheacht do shuíomh in idirghníomhaíochtaí le cúnamh AI agus deiseanna ábhair féideartha. Díríonn sé ar chuairteanna ar dhoiciméid agus ar chomhaid meán cosúil le PDF, Word, Excel agus MP4 amháin, gan URLanna leathanaigh HTML a áireamh — atá clúdaithe i dtuarascáil ar leith atá tiomnaithe do leathanaigh.\nTá sé tábhachtach a thabhairt faoi deara nár fhéach daoine ar aon cheann de na doiciméid seo ar an mbealach traidisiúnta — eascraíonn gach iarratas ó chúntóirí AI a fhaigheann ábhar go huathoibríoch.\nFaoi láthair, ní chuimsíonn na tuarascálacha seo ach iarratais ó chomhrábotanna AI nach ndéanann JavaScript a fhorghníomhú. Ní chuimsíonn siad trácht ó chraoltóirí AI a úsáidtear chun samhlacha AI a oiliúint ná ó ghníomhairí AI atá in ann JavaScript a fhorghníomhú.", + "AIChatbotsContentDocumentsTitle": "Doiciméid", + "AIChatbotsContentPagesDocumentation": "Taispeánann an tuarascáil seo cé na URLanna leathanaigh HTML a ndearnadh rochtain orthu ag Cúntóirí AI (cosúil le ChatGPT) chun faisnéis fhíor-ama a aisghabháil le linn comhráite úsáideoirí. Cabhraíonn sé leat a aithint cé na codanna de do shuíomh Gréasáin a bhfuil córais AI ag tagairt dóibh, ag tabhairt léargais ar infheictheacht do shuíomh in idirghníomhaíochtaí le cúnamh AI agus deiseanna ábhair féideartha. Díríonn sé ar chuairteanna ar leathanaigh ghréasáin chaighdeánacha amháin, gan íoslódálacha comhad ar nós PDFanna, Word, nó doiciméid Excel a áireamh - atá clúdaithe i dtuarascáil ar leith atá tiomnaithe do rochtain ar dhoiciméid.\nTá sé tábhachtach a thabhairt faoi deara nár fhéach daoine ar aon cheann de na leathanaigh seo ar an mbealach traidisiúnta - eascraíonn na hiarratais go léir ó chúntóirí AI a fhaigheann ábhar go huathoibríoch.\nFaoi láthair, ní chuimsíonn na tuarascálacha seo ach iarratais ó róbónna AI nach ndéanann JavaScript a fhorghníomhú. Ní chuimsíonn siad trácht ó chraoltóirí AI a úsáidtear chun samhlacha AI a oiliúint ná ó ghníomhairí AI atá in ann JavaScript a fhorghníomhú.", + "AIChatbotsContentRequests": "Iarratais ar Ábhar Comhrábot AI", + "AIChatbotsContentRequestsHelp1": "Taispeánann an leathanach tuairiscithe seo cé na leathanaigh agus na doiciméid ar do shuíomh Gréasáin a d'iarr comhrábotanna AI ar nós ChatGPT agus botanna móra comhchosúla bunaithe ar mhúnla teanga. Tugann na tuarascálacha seo sonraí faoi na leathanaigh HTML agus na comhaid in-íoslódála (cosúil le PDFanna, Word, nó doiciméid Excel) a ndearnadh rochtain orthu, mar aon leo siúd nárbh fhéidir a aisghabháil mar gheall ar earráidí. Cuidíonn sé seo leat tuiscint a fháil ar an ábhar a bhfuil córais AI ag tagairt dó, acmhainní briste nó do-rochtana a aithint, agus a chinntiú go soláthraíonn do shuíomh Gréasáin rochtain iontaofa ar fhaisnéis a úsáideann uirlisí atá á dtiomáint ag AI.", + "AIChatbotsContentRequestsHelp2": "Tá sé tábhachtach a thabhairt faoi deara nár fhéach daoine ar aon cheann de na leathanaigh ná na doiciméid seo ar an mbealach traidisiúnta — eascraíonn na hiarratais uile ó chúntóirí AI a bhailíonn ábhar go huathoibríoch.", + "AIChatbotsContentRequestsHelp3": "Faoi láthair, ní chuimsíonn na tuarascálacha seo ach iarratais ó róbónna AI nach ndéanann JavaScript a fhorghníomhú, agus ní chuimsíonn siad trácht ó chraoltóirí AI a úsáidtear le haghaidh oiliúna samhlacha nó gníomhairí AI atá in ann JavaScript a rith.", + "ColumnAvgResponseSize": "Meánmhéid Freagartha", + "ColumnAvgResponseSizeDocumentation": "Meánmhéid na bhfreagraí a tugadh ar ais do na hiarratais seo. Braitheann sé seo ar rianú freastalaí nó CDN agus d'fhéadfadh sé a bheith folamh mura bhfuil sé cumraithe, nó folamh le haghaidh URL nuair nach raibh na sonraí seo ag aon cheann dá iarratais.", + "ColumnAvgServerTime": "Meán-Am Freastalaí", + "ColumnAvgServerTimeDocumentation": "Meán-am a thóg sé ar an bhfreastalaí freagairt do na hiarratais seo. Braitheann sé seo ar rianú an fhreastalaí nó an CDN agus d'fhéadfadh sé a bheith folamh mura bhfuil sé cumraithe, nó folamh le haghaidh URL nuair nach raibh na sonraí seo ag aon cheann dá iarratais.", + "ColumnPageNotFound404Requests": "Iarratais Leathanach Gan Aimsiú (404)", + "ColumnPageNotFound404RequestsDocumentation": "Líon na n-iarratas bot a thug freagra HTTP 404 (Níor aimsíodh) nó 410 (Imithe) ar ais.", + "ColumnServerError5xxRequests": "Iarratais 5XX", + "ColumnServerError5xxRequestsDocumentation": "Líon na n-iarratas bot a thug freagra HTTP 5xx (earráid freastalaí) ar ais.", + "ColumnTotalBrokenRequests": "Líon Iomlán na nIarratas Briste", + "ColumnTotalBrokenRequestsDocumentation": "Líon na n-iarratas bot a thug freagra HTTP 404 (Níor aimsíodh), 410 (Imithe), nó 5xx (earráid freastalaí) ar ais.", + "AIChatbotsAIFavouredPagesDocumentation": "Taispeánann sé leathanaigh a iarrann comhrábotanna AI go minic ach nach dtugann daoine cuairt orthu go minic. Úsáideach chun ábhar a aimsiú a thagann chun cinn nó a dtagraíonn córais AI dó ach nach meallann mórán rannpháirtíochta dírí daonna go fóill.", + "AIChatbotsAIFavouredPagesTitle": "Leathanaigh is Fearr le hintleacht shaorga", + "AIChatbotsHumanFavouredPagesDocumentation": "Taispeánann sé leathanaigh a fhaigheann i bhfad níos mó cuairteanna ó dhaoine ná ó chomhrábotanna AI. Úsáideach chun ábhar a aithint a bhfuil baint láidir aige le fíorúsáideoirí ach nach bhfuil ionadaíocht leordhóthanach air i dtrácht nó i moltaí atá á dtiomáint ag AI.", + "AIChatbotsHumanFavouredPagesTitle": "Leathanaigh is Fearr le Daoine", + "ColumnAIChatbotRequests": "Iarratais ar Chomhrábot AI", + "ColumnAIChatbotRequestsDocumentation": "Líon iomlán na n-iarr comhrábot AI an URL seo le linn na tréimhse roghnaithe.", + "ColumnDiscrepancyScore": "Scór Neamhréireachta", + "ColumnDiscrepancyScoreAIFavouredDocumentation": "Scór ó 0 go 100 a chomhcheanglaíonn cé chomh láidir is atá an leathanach seo i dtreo comhrábots AI (cé mhéad níos mó a iarrann comhrábots air ná mar a thugann daoine cuairt air) le cé mhéad tráchta comhrábots atá air i gcomparáid leis an leathanach is mó a iarrtar. Ciallaíonn níos airde go ndíríonn comhrábots AI air i bhfad níos mó ná daoine; faigheann leathanaigh atá cothrom nó atá claonta i dtreo daoine scór 0.", + "ColumnDiscrepancyScoreHumanFavouredDocumentation": "Scór ó 0 go 100 a chomhcheanglaíonn cé chomh láidir is atá an leathanach seo i dtreo daoine (cé mhéad níos mó cuairte a thugann daoine air ná mar a iarrann comhrábotanna AI) le cé mhéad tráchta daonna atá air i gcomparáid leis an leathanach is mó cuairte. Ciallaíonn níos airde go mbíonn daoine i bhfad níos mó idirghníomhaíochta leis ná mar a dhéanann comhrábotanna AI; faigheann leathanaigh atá cothrom nó atá claonta i dtreo AI scór 0.", + "ColumnUniqueHumanPageviews": "Amharcanna Leathanaigh Dhaonna Uathúla", + "ColumnUniqueHumanPageviewsDocumentation": "Líon na gcuairteanna inar fhéach úsáideoirí fíor ar an URL seo uair amháin ar a laghad. Ní chuirtear trácht ó chomhrábots AI agus bots eile san áireamh.", + "AIChatbotsLast30MinutesDocumentation": "Taispeánann sé iarratais ar chomhrábot AI a taifeadadh le linn na 30 nóiméad seo caite, grúpáilte de réir comhrábot.", + "AIChatbotsLast30MinutesTitle": "Comhrábotanna AI - 30 Nóiméad Deiridh", + "AIChatbotsLast8HoursDocumentation": "Taispeánann sé iarratais ar chomhrábot AI a taifeadadh le linn na 8 n-uaire an chloig seo caite, grúpáilte de réir comhrábot.", + "AIChatbotsLast8HoursTitle": "Comhrábotanna AI - 8 nUair an Chloig Deiridh", + "AIChatbotsRealtime": "Comhrábots AI i bhfíor-am", + "AIChatbotsRealtimeHelp1": "Taispeánann leathanach Fíor-ama Chatbots AI gníomhaíocht le déanaí ó chomhrábot AI ó shonraí rianaithe bot amha, lena n-áirítear méid iarratas comhrábot, URLanna leathanaigh uathúla, agus comhaireamh earráide HTTP.", + "AIChatbotsRealtimeHelp2": "Tá na tuarascálacha seo teoranta do fhuinneoga gearra fíor-ama ionas go bhfanfaidh siad gasta agus intuartha ar shuíomhanna móra.", + "AIChatbotsRealTimeReportLimitFooter": "Tá an tuarascáil seo teoranta do na %1$s sraitheanna barr.", + "TopPageUrlsLast30MinutesDocumentation": "Taispeánann sé URLanna leathanaigh arna n-iarraidh ag comhrábotanna AI le 30 nóiméad anuas, in ord de réir iarratais comhrábot.", + "TopPageUrlsLast30MinutesTitle": "URLanna na Leathanaigh is Fearr - 30 nóiméad deireanacha", + "TopPageUrlsLast8HoursDocumentation": "Taispeánann sé URLanna leathanaigh arna n-iarraidh ag comhrábotanna AI le linn na 8 n-uaire an chloig seo caite, in ord de réir iarratais comhrábot.", + "TopPageUrlsLast8HoursTitle": "URLanna Barrleathanaigh - 8 n-uaire an chloig dheireanacha" + } +} diff --git a/plugins/BotTracking/lang/gl.json b/plugins/BotTracking/lang/gl.json new file mode 100644 index 0000000000000000000000000000000000000000..e9423aad2110b759555242ea1be48db30e11fb79 --- /dev/null +++ b/plugins/BotTracking/lang/gl.json @@ -0,0 +1,6 @@ +{ + "BotTracking": { + "PageUrl": "URL da páxina", + "SiteWithoutDataBackToMatomo": "Voltar a Matomo" + } +} diff --git a/plugins/BotTracking/lang/gu.json b/plugins/BotTracking/lang/gu.json new file mode 100644 index 0000000000000000000000000000000000000000..0c7ef7fafc0cea6b81119640f938da3a81fe9a34 --- /dev/null +++ b/plugins/BotTracking/lang/gu.json @@ -0,0 +1,11 @@ +{ + "BotTracking": { + "PageUrl": "પેજનું યુ.આર.એલ", + "NoDataHttpTrackingApi": "HTTP ટ્રેકિંગ API", + "SiteWithoutDataBackToMatomo": "Matomo પર પાછા", + "SiteWithoutDataInstallWithX": "%1$s સાથે ઇન્સ્ટોલ કરો", + "SiteWithoutDataStepByStepGuide": "સ્ટેપ-બાય-સ્ટેપ માર્ગદર્શિકા", + "AIChatbotsContentPagesTitle": "પૃષ્ઠો", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/he.json b/plugins/BotTracking/lang/he.json new file mode 100644 index 0000000000000000000000000000000000000000..2d78178b0cd533a5439f2907764af51888298488 --- /dev/null +++ b/plugins/BotTracking/lang/he.json @@ -0,0 +1,7 @@ +{ + "BotTracking": { + "PageUrl": "כתובת העמוד", + "SiteWithoutDataBackToMatomo": "חזרה ל Matomo", + "AIChatbotsContentPagesTitle": "עמודים" + } +} diff --git a/plugins/BotTracking/lang/hi.json b/plugins/BotTracking/lang/hi.json new file mode 100644 index 0000000000000000000000000000000000000000..753d63e86414920577e44931ee8a83316c26ebf4 --- /dev/null +++ b/plugins/BotTracking/lang/hi.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "पृष्ठ का यू.आर.एल", + "SiteWithoutDataBackToMatomo": "वापस Matomo तक", + "AIChatbotsContentPagesTitle": "पृष्ठों", + "ContentUrl": "यूआरएल" + } +} diff --git a/plugins/BotTracking/lang/hr.json b/plugins/BotTracking/lang/hr.json new file mode 100644 index 0000000000000000000000000000000000000000..6d40a41a191af9045b649ba0aaa3353bb823479a --- /dev/null +++ b/plugins/BotTracking/lang/hr.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "URL stranice", + "SiteWithoutDataBackToMatomo": "Natrag na Matomo", + "AIChatbotsContentPagesTitle": "Stranice", + "ContentUrl": "網址" + } +} diff --git a/plugins/BotTracking/lang/hu.json b/plugins/BotTracking/lang/hu.json new file mode 100644 index 0000000000000000000000000000000000000000..73a907739c066fea971ec91476d98a5cbb726305 --- /dev/null +++ b/plugins/BotTracking/lang/hu.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "Weblap URL-je", + "SiteWithoutDataBackToMatomo": "Vissza a Matomohoz", + "AIChatbotsContentPagesTitle": "weblap", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/hy.json b/plugins/BotTracking/lang/hy.json new file mode 100644 index 0000000000000000000000000000000000000000..482bab67e6bc75348918e0a845d46a7fc3555139 --- /dev/null +++ b/plugins/BotTracking/lang/hy.json @@ -0,0 +1,5 @@ +{ + "BotTracking": { + "PageUrl": "Էջի URL" + } +} diff --git a/plugins/BotTracking/lang/id.json b/plugins/BotTracking/lang/id.json new file mode 100644 index 0000000000000000000000000000000000000000..5430889a1219e6ca31652fa563f39cadb4cdc5ab --- /dev/null +++ b/plugins/BotTracking/lang/id.json @@ -0,0 +1,100 @@ +{ + "BotTracking": { + "PageUrl": "URL Halaman", + "NoDataHttpTrackingApi": "HTTP Tracking API", + "SiteWithoutDataBackToMatomo": "Kembali ke Matomo", + "ReportTitleChatbotsOverview": "Ringkasan Chatbot AI", + "AIChatbotsOverviewHelp1": "Halaman Ringkasan Chatbot AI memberikan wawasan tentang lalu lintas situs web yang berasal dari chatbot AI seperti ChatGPT dan asisten lain berbasis model bahasa besar. Laporan ini melacak metrik utama termasuk jumlah permintaan yang dibuat oleh bot tersebut, halaman dan dokumen yang mereka akses, serta kesalahan yang ditemukan. Laporan ini juga menawarkan rincian mendetail yang menunjukkan bot mana yang mengunjungi URL halaman tertentu, membantu Anda memahami bagaimana chatbot AI berinteraksi dengan konten Anda dan mengidentifikasi peluang untuk meningkatkan visibilitas dan aksesibilitas bagi pengguna yang digerakkan AI.", + "AIChatbotsOverviewHelp2": "Penting untuk dicatat bahwa tidak satu pun dari halaman ini benar-benar dilihat oleh manusia dalam cara tradisional — semua permintaan berasal dari chatbot AI yang mengambil konten secara otomatis.", + "AIChatbotsOverviewHelp3": "Saat ini, laporan ini secara eksklusif hanya mencakup permintaan dari chatbot AI yang tidak menjalankan JavaScript. Laporan ini tidak mencakup lalu lintas dari crawler AI yang digunakan untuk melatih model AI atau dari agen AI yang mampu menjalankan JavaScript.", + "AIChatbotsReportDocumentation": "Temukan chatbot AI mana yang sedang merayapi situs Anda dan berapa banyak hit yang dihasilkan masing-masing. Perluas chatbot untuk meninjau halaman atau dokumen yang paling sering diminta.", + "AIChatbotsReportTitle": "Chatbot AI", + "ChatbotsOverTimeReportDocumentation": "Laporan ini memetakan perkembangan aktivitas chatbot AI. Klik metrik mana pun untuk memperbesar grafiknya dan meninjau tren yang mendasarinya.", + "ColumnAIChatbotName": "Nama Chatbot AI", + "ColumnAcquiredVisits": "Kunjungan yang diperoleh", + "ColumnAcquiredVisitsDocumentation": "Kunjungan yang dimulai setelah seseorang mengeklik dari chatbot AI (Perujuk → Asisten AI).", + "ColumnClickThroughRate": "Rasio klik-tayang", + "ColumnClickThroughRateDocumentation": "Bagian permintaan chatbot AI yang menghasilkan kunjungan yang diperoleh (kunjungan yang diperoleh ÷ permintaan).", + "ColumnDocumentRequests": "Permintaan Dokumen", + "ColumnDocumentRequestsDocumentation": "Jumlah total permintaan bot ke URL dokumen yang tercatat selama periode yang dipilih.", + "ColumnNotFoundRequests": "Permintaan tidak ditemukan", + "ColumnNotFoundRequestsDocumentation": "Jumlah permintaan bot yang mengembalikan respons HTTP 404 atau 410, termasuk URL dokumen.", + "ColumnPageRequests": "Permintaan Halaman", + "ColumnPageRequestsDocumentation": "Jumlah total permintaan bot ke URL halaman yang tercatat selama periode yang dipilih.", + "ColumnRequests": "Permintaan", + "ColumnRequestsDocumentation": "Jumlah total permintaan bot yang tercatat selama periode yang dipilih. Termasuk URL halaman dan dokumen.", + "ColumnServerErrorRequests": "Permintaan kesalahan server (5xx)", + "ColumnServerErrorRequestsDocumentation": "Jumlah permintaan bot yang mengembalikan respons HTTP 5xx, termasuk URL dokumen.", + "ColumnUniqueAiChatbots": "Chatbot AI unik", + "ColumnUniqueAiChatbotsDocumentation": "User agent chatbot AI yang terdeteksi selama periode yang dipilih.", + "ColumnUniqueDocumentUrls": "URL dokumen unik", + "ColumnUniqueDocumentUrlsDocumentation": "URL unduhan atau dokumen berbeda yang diminta oleh chatbot AI.", + "ColumnUniquePageUrls": "URL halaman unik", + "ColumnUniquePageUrlsDocumentation": "URL halaman berbeda yang diminta oleh chatbot AI.", + "DetectingYourSite": "Mendeteksi situs Anda…", + "DocumentUrl": "URL Dokumen", + "NoDataAmazonCloudFrontInstructions1": "Integrasi ini menggunakan AWS Lambda yang membaca log akses Amazon CloudFront dari S3 dan mengirim hit Matomo secara batch. Kode tracker dipelihara secara eksternal, jadi ikuti README repositori untuk detail pengaturan dan deployment terkini.", + "NoDataAmazonCloudFrontInstructions2": "Deploy Lambda (Node.js) dan atur URL Matomo serta ID situs Anda.", + "NoDataAmazonCloudFrontInstructions3": "Lampirkan trigger S3 untuk objek log Amazon CloudFront (misalnya, ObjectCreated pada log .gz).", + "NoDataAmazonCloudFrontInstructions4": "Verifikasi pengaturan dengan memeriksa bahwa hit Chatbot AI muncul dalam laporan pelacakan.", + "NoDataCloudflareInstructions1": "Integrasi ini menggunakan Cloudflare Worker yang mem-proxy permintaan ke origin Anda dan secara asinkron mengirim hit Matomo tanpa menunda respons. Kode tracker dipelihara secara eksternal, jadi ikuti README repositori untuk detail pengaturan dan deployment terkini.", + "NoDataCloudflareInstructions2": "Deploy Worker (Node.js) dan atur URL Matomo serta ID situs Anda.", + "NoDataCloudflareInstructions3": "Lampirkan Worker ke zone atau route Anda agar berjalan langsung bersama permintaan.", + "NoDataCloudflareInstructions4": "Verifikasi pengaturan dengan memeriksa bahwa hit Chatbot AI muncul dalam laporan pelacakan.", + "NoDataHttpTrackingApiDescription": "HTTP Tracking API memungkinkan Anda melacak permintaan chatbot AI secara langsung ketika Anda memerlukan integrasi kustom. Mulailah dengan %1$sbasis pengetahuan Tracking API%2$s dan gunakan %3$sreferensi HTTP Tracking API%4$s untuk parameter dan perilaku pelacakan bot terbaru.", + "NoDataLinkKnowledgeBase": "Dokumentasi: %1$sBasis Pengetahuan Matomo%2$s", + "NoDataLinkSourceCode": "Kode sumber: %1$srepositori GitHub%2$s", + "NoDataMissingOneContactUs": "Ada yang kurang? Hubungi kami", + "NoDataWordPressInstructions1": "Integrasi WordPress menambahkan bridge kecil yang mengirim sinyal Chatbot AI ke Matomo. Karena tracker dipelihara secara eksternal, README adalah sumber utama untuk langkah-langkah terkini.", + "NoDataWordPressInstructions2": "Instal dan aktifkan plugin.", + "NoDataWordPressInstructions3": "Masukkan URL Matomo dan ID situs Anda.", + "NoDataWordPressInstructions4": "Verifikasi pengaturan dengan memeriksa bahwa hit Chatbot AI muncul dalam laporan pelacakan.", + "NoRecentAIBotRequests": "%1$sTidak ada data yang dikumpulkan dalam 7 hari terakhir.%2$s %3$sSiapkan pelacakan Chatbot AI sekarang%4$s.", + "NoRecentRequestsWidgetTitle": "Chatbot AI - Tidak Ada Permintaan Terbaru", + "PluginDescription": "Lacak dan analisis kunjungan dari chatbot AI seperti ChatGPT-User dan yang serupa. Plugin ini membantu Anda memahami bagaimana alat yang digerakkan AI berinteraksi dengan situs web Anda dengan mencatat dan melaporkan permintaan yang dipicu bot.", + "ReportTitleChatbotsOverTime": "Chatbot AI dari Waktu ke Waktu", + "SegmentNotSupported": "Laporan tidak mendukung segmentasi. Data yang ditampilkan adalah data laporan standar Anda yang tidak disegmentasi.", + "SiteWithoutDataChooseTrackingMethod": "Pilih Metode Pelacakan Chatbot AI Anda", + "SiteWithoutDataChooseTrackingMethodPreamble1": "Lacak chatbot AI yang mengakses situs web Anda untuk memahami bagaimana konten Anda ditemukan dan dikonsumsi oleh asisten bertenaga AI.", + "SiteWithoutDataChooseTrackingMethodPreamble2": "Chatbot AI biasanya tidak menjalankan JavaScript. Akibatnya, mereka tidak dilacak oleh analitik berbasis JavaScript secara default, dan diperlukan konfigurasi pelacakan tambahan.", + "SiteWithoutDataInstallWithX": "Instal dengan %1$s", + "SiteWithoutDataInstallWithXRecommendation": "Instal pelacakan Chatbot AI dengan %1$s (direkomendasikan untuk Anda)", + "SiteWithoutDataNotYetReady": "Belum siap menyiapkan Pelacakan Chatbot AI sekarang?", + "SiteWithoutDataOtherInstallMethods": "Metode lain untuk melacak Chatbot AI", + "SiteWithoutDataOtherInstallMethodsIntro": "Kami menawarkan beberapa jalur pelacakan - pilih yang sesuai dengan pengaturan Anda.", + "SiteWithoutDataRecommendationText": "Kami telah mendeteksi bahwa situs web Anda menggunakan %1$s, sehingga Anda dapat menyiapkan pelacakan Chatbot AI menggunakan integrasi resmi %1$s kami.", + "SiteWithoutDataStepByStepGuide": "Panduan langkah demi langkah", + "AIChatbotsContentPagesTitle": "Halaman", + "ContentUrl": "URL", + "AIChatbotsBrokenContentTitle": "Halaman dan Dokumen Rusak", + "AIChatbotsContentDocumentsDocumentation": "Laporan ini menunjukkan URL dokumen mana yang diakses oleh chatbot AI (seperti ChatGPT) untuk mengambil informasi real-time selama percakapan pengguna. Laporan ini membantu Anda mengidentifikasi bagian mana dari situs web Anda yang dirujuk oleh sistem AI, sehingga memberikan wawasan tentang visibilitas situs Anda dalam interaksi yang dibantu AI serta peluang konten yang potensial. Laporan ini hanya berfokus pada kunjungan ke dokumen dan file media seperti PDF, Word, Excel, dan MP4, serta tidak mencakup URL halaman HTML — yang dibahas dalam laporan terpisah yang khusus untuk halaman.\n\nPenting untuk dicatat bahwa tidak satu pun dari dokumen ini benar-benar dilihat oleh manusia dengan cara tradisional — semua permintaan berasal dari asisten AI yang mengambil konten secara otomatis.\n\nSaat ini, laporan ini secara eksklusif hanya mencakup permintaan dari chatbot AI yang tidak menjalankan JavaScript. Laporan ini tidak mencakup traffic dari crawler AI yang digunakan untuk melatih model AI atau dari agen AI yang mampu menjalankan JavaScript.", + "AIChatbotsContentDocumentsTitle": "Dokumen", + "AIChatbotsContentPagesDocumentation": "Laporan ini menunjukkan URL halaman HTML mana yang diakses oleh asisten AI (seperti ChatGPT) untuk mengambil informasi real-time selama percakapan pengguna. Laporan ini membantu Anda mengidentifikasi bagian mana dari situs web Anda yang dirujuk oleh sistem AI, sehingga memberikan wawasan tentang visibilitas situs Anda dalam interaksi yang dibantu AI serta peluang konten yang potensial. Laporan ini hanya berfokus pada kunjungan ke halaman web standar, dan tidak mencakup unduhan file seperti dokumen PDF, Word, atau Excel — yang dibahas dalam laporan terpisah yang khusus untuk akses dokumen.\n\nPenting untuk dicatat bahwa tidak satu pun dari halaman ini benar-benar dilihat oleh manusia dengan cara tradisional — semua permintaan berasal dari asisten AI yang mengambil konten secara otomatis.\n\nSaat ini, laporan ini secara eksklusif hanya mencakup permintaan dari bot AI yang tidak menjalankan JavaScript. Laporan ini tidak mencakup traffic dari crawler AI yang digunakan untuk melatih model AI atau dari agen AI yang mampu menjalankan JavaScript.", + "AIChatbotsContentRequests": "Permintaan Konten Chatbot AI", + "AIChatbotsContentRequestsHelp1": "Halaman laporan ini menunjukkan halaman dan dokumen mana di situs web Anda yang diminta oleh chatbot AI seperti ChatGPT dan bot serupa berbasis large language model. Laporan ini merinci halaman HTML dan file yang dapat diunduh (seperti PDF, Word, atau Excel) yang diakses, beserta file yang tidak dapat diambil karena error. Hal ini membantu Anda memahami konten mana yang dirujuk oleh sistem AI, mengidentifikasi resource yang rusak atau tidak dapat diakses, serta memastikan situs web Anda menyediakan akses yang andal ke informasi yang digunakan oleh alat berbasis AI.", + "AIChatbotsContentRequestsHelp2": "Penting untuk dicatat bahwa tidak satu pun dari halaman atau dokumen ini benar-benar dilihat oleh manusia dengan cara tradisional — semua permintaan berasal dari asisten AI yang mengambil konten secara otomatis.", + "AIChatbotsContentRequestsHelp3": "Saat ini, laporan ini hanya mencakup permintaan dari bot AI yang tidak menjalankan JavaScript, dan tidak mencakup traffic dari crawler AI yang digunakan untuk pelatihan model atau agen AI yang mampu menjalankan JavaScript.", + "AIChatbotsOverview": "Ringkasan Chatbot AI", + "ColumnAvgResponseSize": "Rata-rata Ukuran Respons", + "ColumnAvgResponseSizeDocumentation": "Ukuran rata-rata respons yang dikembalikan untuk permintaan ini. Ini bergantung pada pelacakan server atau CDN dan mungkin kosong jika tidak dikonfigurasi, atau kosong untuk URL tertentu jika tidak ada permintaannya yang membawa data ini.", + "ColumnAvgServerTime": "Rata-rata Waktu Server", + "ColumnAvgServerTimeDocumentation": "Rata-rata waktu yang dibutuhkan server untuk merespons permintaan ini. Ini bergantung pada pelacakan server atau CDN dan mungkin kosong jika tidak dikonfigurasi, atau kosong untuk URL tertentu jika tidak ada permintaannya yang membawa data ini.", + "ColumnPageNotFound404Requests": "Permintaan Halaman Tidak Ditemukan (404)", + "ColumnPageNotFound404RequestsDocumentation": "Jumlah permintaan bot yang menghasilkan respons HTTP 404 (Not Found) atau 410 (Gone).", + "ColumnServerError5xxRequests": "Permintaan 5XX", + "ColumnServerError5xxRequestsDocumentation": "Jumlah permintaan bot yang menghasilkan respons HTTP 5xx (server error).", + "ColumnTotalBrokenRequests": "Total Permintaan Rusak", + "ColumnTotalBrokenRequestsDocumentation": "Jumlah permintaan bot yang menghasilkan respons HTTP 404 (Not Found), 410 (Gone), atau 5xx (server error).", + "AIChatbotsAIFavouredPagesTitle": "Halaman yang Disukai AI", + "AIChatbotsBrokenContentDocumentation": "Laporan ini mencantumkan URL halaman rusak dan URL dokumen (seperti file PDF, Word, atau Excel) yang diakses oleh chatbot AI (seperti ChatGPT) tetapi mengembalikan error atau tidak tersedia. Ini membantu mengidentifikasi konten yang hilang atau tidak dapat diakses yang coba diambil oleh LLM, sehingga Anda dapat memperbaiki tautan rusak dan meningkatkan keandalan serta keterjangkauan situs Anda.", + "AIChatbotsHumanFavouredPagesDocumentation": "Menampilkan halaman yang menerima kunjungan jauh lebih banyak dari manusia dibandingkan dari chatbot AI. Berguna untuk mengidentifikasi konten yang sangat relevan bagi pengguna nyata tetapi kurang terwakili dalam trafik atau rekomendasi berbasis AI.", + "AIChatbotsHumanFavouredPagesTitle": "Halaman yang Disukai Manusia", + "ColumnAIChatbotRequests": "Permintaan Chatbot AI", + "ColumnAIChatbotRequestsDocumentation": "Jumlah total berapa kali URL ini diminta oleh chatbot AI selama periode yang dipilih.", + "ColumnDiscrepancyScore": "Skor Kesenjangan", + "ColumnDiscrepancyScoreAIFavouredDocumentation": "Skor dari 0 hingga 100 yang menggabungkan seberapa kuat halaman ini condong ke chatbot AI (seberapa jauh lebih sering diminta oleh chatbot dibanding dikunjungi manusia) dengan seberapa besar trafik chatbot-nya relatif terhadap halaman yang paling sering diminta. Semakin tinggi, semakin besar chatbot AI berfokus pada halaman ini dibanding manusia; halaman yang seimbang atau lebih condong ke manusia mendapat skor 0.", + "ColumnDiscrepancyScoreHumanFavouredDocumentation": "Skor dari 0 hingga 100 yang menggabungkan seberapa kuat halaman ini condong ke manusia (seberapa jauh lebih sering dikunjungi manusia dibanding diminta oleh chatbot AI) dengan seberapa besar trafik manusia-nya relatif terhadap halaman yang paling sering dikunjungi. Semakin tinggi, semakin besar keterlibatan manusia pada halaman ini dibanding chatbot AI; halaman yang seimbang atau lebih condong ke AI mendapat skor 0.", + "ColumnUniqueHumanPageviews": "Tayangan Halaman Unik oleh Manusia", + "ColumnUniqueHumanPageviewsDocumentation": "Jumlah kunjungan ketika pengguna nyata melihat URL ini setidaknya satu kali. Trafik dari chatbot AI dan bot lainnya tidak dihitung." + } +} diff --git a/plugins/BotTracking/lang/is.json b/plugins/BotTracking/lang/is.json new file mode 100644 index 0000000000000000000000000000000000000000..b270872c76913957ae9bac0aabf216ad05a1eecc --- /dev/null +++ b/plugins/BotTracking/lang/is.json @@ -0,0 +1,7 @@ +{ + "BotTracking": { + "PageUrl": "Síðu slóð", + "SiteWithoutDataBackToMatomo": "Aftur í Matomo", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/it.json b/plugins/BotTracking/lang/it.json new file mode 100644 index 0000000000000000000000000000000000000000..5dc8fc2825442a854f8532bb430e672db00af301 --- /dev/null +++ b/plugins/BotTracking/lang/it.json @@ -0,0 +1,11 @@ +{ + "BotTracking": { + "PageUrl": "URL pagina", + "NoDataHttpTrackingApi": "API di Tracciamento HTTP", + "SiteWithoutDataBackToMatomo": "Torna a Matomo", + "SiteWithoutDataInstallWithX": "Installa con %1$s", + "SiteWithoutDataStepByStepGuide": "Guida passo-passo", + "AIChatbotsContentPagesTitle": "Pagine", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/ja.json b/plugins/BotTracking/lang/ja.json new file mode 100644 index 0000000000000000000000000000000000000000..f5ba632e0713ef84ee79056c4c295e94c415acf7 --- /dev/null +++ b/plugins/BotTracking/lang/ja.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "ページ URL", + "NoDataHttpTrackingApi": "HTTPトラッキング API", + "SiteWithoutDataBackToMatomo": "Matomo へ戻る", + "AIChatbotsContentPagesTitle": "ページ", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/ka.json b/plugins/BotTracking/lang/ka.json new file mode 100644 index 0000000000000000000000000000000000000000..0749a1ef14c2c171b4b062314b9778059d02b5ad --- /dev/null +++ b/plugins/BotTracking/lang/ka.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "გვერდის URL", + "SiteWithoutDataBackToMatomo": "Matomo–ზე დაბრუნება", + "AIChatbotsContentPagesTitle": "გვერდები", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/ko.json b/plugins/BotTracking/lang/ko.json new file mode 100644 index 0000000000000000000000000000000000000000..69d339e5bbb064ee77e029bfeb2c789270d9c5c0 --- /dev/null +++ b/plugins/BotTracking/lang/ko.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "페이지 URL", + "SiteWithoutDataBackToMatomo": "Matomo으로 돌아가기", + "AIChatbotsContentPagesTitle": "페이지", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/ku.json b/plugins/BotTracking/lang/ku.json new file mode 100644 index 0000000000000000000000000000000000000000..b62a37bd869ce86e2e4347c80b42de2793ddc1f8 --- /dev/null +++ b/plugins/BotTracking/lang/ku.json @@ -0,0 +1,5 @@ +{ + "BotTracking": { + "SiteWithoutDataBackToMatomo": "Vegere Matomo" + } +} diff --git a/plugins/BotTracking/lang/lb.json b/plugins/BotTracking/lang/lb.json new file mode 100644 index 0000000000000000000000000000000000000000..6277fcedd81a70389be0fcbd5e72f6df095a345f --- /dev/null +++ b/plugins/BotTracking/lang/lb.json @@ -0,0 +1,6 @@ +{ + "BotTracking": { + "PageUrl": "Säiten URL", + "ContentUrl": "Adress" + } +} diff --git a/plugins/BotTracking/lang/lt.json b/plugins/BotTracking/lang/lt.json new file mode 100644 index 0000000000000000000000000000000000000000..a3f8d8e81864411cb2ddfdcae79e7c710f991bfd --- /dev/null +++ b/plugins/BotTracking/lang/lt.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "Puslapio URL", + "NoDataHttpTrackingApi": "HTTP sekimo API", + "SiteWithoutDataBackToMatomo": "Atgal į Matomo", + "AIChatbotsContentPagesTitle": "Puslapiai", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/lv.json b/plugins/BotTracking/lang/lv.json new file mode 100644 index 0000000000000000000000000000000000000000..8ae464d5c70b09bf6da2653ddd3210056b33159f --- /dev/null +++ b/plugins/BotTracking/lang/lv.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "Lapas URL", + "NoDataHttpTrackingApi": "HTTP izsekošanas API", + "SiteWithoutDataBackToMatomo": "Atgriezties uz Matomo", + "AIChatbotsContentPagesTitle": "Lapas", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/ms.json b/plugins/BotTracking/lang/ms.json new file mode 100644 index 0000000000000000000000000000000000000000..c6aad623a737c0f4a45061dcae8214f8867b979a --- /dev/null +++ b/plugins/BotTracking/lang/ms.json @@ -0,0 +1,5 @@ +{ + "BotTracking": { + "PageUrl": "Laman URL" + } +} diff --git a/plugins/BotTracking/lang/nb.json b/plugins/BotTracking/lang/nb.json new file mode 100644 index 0000000000000000000000000000000000000000..8ec41db60c06845adeb4b8e3003d217e679943bf --- /dev/null +++ b/plugins/BotTracking/lang/nb.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "Side-URL", + "NoDataHttpTrackingApi": "API for HTTP-sporing", + "SiteWithoutDataBackToMatomo": "Tilbake til Matomo", + "AIChatbotsContentPagesTitle": "Sider", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/nl.json b/plugins/BotTracking/lang/nl.json new file mode 100644 index 0000000000000000000000000000000000000000..dde6e6987d1486a7961d1f7b60c4c5130fb51289 --- /dev/null +++ b/plugins/BotTracking/lang/nl.json @@ -0,0 +1,11 @@ +{ + "BotTracking": { + "PageUrl": "Pagina-URL", + "NoDataHttpTrackingApi": "HTTP Tracking API", + "SiteWithoutDataBackToMatomo": "Terug naar Matomo", + "SiteWithoutDataInstallWithX": "Installeren met %1$s", + "SiteWithoutDataStepByStepGuide": "Stapsgewijze handleiding", + "AIChatbotsContentPagesTitle": "Pagina's", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/nn.json b/plugins/BotTracking/lang/nn.json new file mode 100644 index 0000000000000000000000000000000000000000..f4bc8a56f7a34b0560db882cebc440a696e79782 --- /dev/null +++ b/plugins/BotTracking/lang/nn.json @@ -0,0 +1,8 @@ +{ + "BotTracking": { + "PageUrl": "Side-URL", + "SiteWithoutDataBackToMatomo": "Attende til Matomo", + "AIChatbotsContentPagesTitle": "Sider", + "ContentUrl": "URL" + } +} diff --git a/plugins/BotTracking/lang/pl.json b/plugins/BotTracking/lang/pl.json new file mode 100644 index 0000000000000000000000000000000000000000..31949d6893f10a32b3b69160017e24ffc166bf69 --- /dev/null +++ b/plugins/BotTracking/lang/pl.json @@ -0,0 +1,9 @@ +{ + "BotTracking": { + "PageUrl": "Adres URL strony", + "NoDataHttpTrackingApi": "API Śledzenia HTTP", + "SiteWithoutDataBackToMatomo": "Powrót do Matomo", + "AIChatbotsContentPagesTitle": "Strony", + "ContentUrl": "URL" + } +}