use Cubist\Matomo\Reporting;
use Illuminate\Support\Facades\Route;
use NumberFormatter;
+
// __('!! Statistiques')
trait StatsOperation
{
];
}
- protected function getMatomoServer($fluidbook_id) {
+ protected function getMatomoServer($fluidbook_id)
+ {
// Get the appropriate server based on the Fluidbook ID
// Stats are split across different servers depending on the ID:
// ID < 21210 = stats3.fluidbook.com
return 'stats5.fluidbook.com';
}
- protected function getMatomoToken($server) : bool|string {
+ protected function getMatomoToken($server): bool|string
+ {
$tokens = $this->getMatomoTokens();
return $tokens[$server] ?? false;
}
- private function getReporting($fluidbook_id) : Reporting {
+ private function getReporting($fluidbook_id): Reporting
+ {
$server = $this->getMatomoServer($fluidbook_id);
$token = $this->getMatomoToken($server);
return new Reporting("https://{$server}/", $token, $fluidbook_id);
}
- private function parseDate($date) {
+ private function parseDate($date)
+ {
// Match possible date strings:
// - YYYY
// - YYYY-MM
extract($date_matches); // Just for easier access to match variables
// Bail out on nonsensical dates
- if(isset($start_date) && isset($end_date) && ($start_date > $end_date)) {
+ if (isset($start_date) && isset($end_date) && ($start_date > $end_date)) {
return false;
}
// Since the report date can be in different formats (ie. YYYY, YYYY-MM or a full range)
// and this format determines the report mode (range, month or year), we have to figure out
// the mode and full starting and ending dates, as necessary
- public function getReportSettings($date, $fluidbook) {
+ public function getReportSettings($date, $fluidbook)
+ {
$date_matches = $this->parseDate($date);
// ...
}
// Figure out the best period to use for the stats according to the date range
- public function determinePeriod($dates, $fluidbook) {
+ public function determinePeriod($dates, $fluidbook)
+ {
// TODO: refactor this into getReportSettings() so that this function can simply take the determined start and end dates, calculate the number of days and give an answer based on that. No extra logic to try to figure out which mode it is...
return 'year';
}
- protected function statsLoader($fluidbook_id, $hash, $date = null, $period_override = null) {
+ protected function statsLoader($fluidbook_id, $hash, $date = null, $period_override = null)
+ {
$report_URL = route('stats-report', compact('fluidbook_id', 'hash', 'date', 'period_override'));
return view('fluidbook_stats.loader', compact('report_URL'));
}
- protected function statsSummary($fluidbook_id, $hash, $date = null, $period_override = null) {
+ protected function statsSummary($fluidbook_id, $hash, $date = null, $period_override = null)
+ {
$locale = app()->getLocale();
$base_URL = route('stats', compact('fluidbook_id', 'hash')); // Used by date range picker to update report URL
// Translatable list of labels for the available periods
$period_map = [
- 'day' => [
- 'singular' => __('Day'),
- 'periodic' => __('Daily'),
+ 'day' => [
+ 'singular' => __('Jour'),
+ 'periodic' => __('Quotidien'),
],
- 'week' => [
- 'singular' => __('Week'),
- 'periodic' => __('Weekly'),
+ 'week' => [
+ 'singular' => __('Semaine'),
+ 'periodic' => __('Hebdomadaire'),
],
'month' => [
- 'singular' => __('Month'),
- 'periodic' => __('Monthly'),
+ 'singular' => __('Mois'),
+ 'periodic' => __('Mensuel'),
],
- 'year' => [
- 'singular' => __('Year'),
- 'periodic' => __('Annual'),
+ 'year' => [
+ 'singular' => __('Année'),
+ 'periodic' => __('Annuel'),
],
];
$period_override = in_array($period_override, array_keys($period_map)) ? $period_override : null;
$period = $period_override ?? $this->determinePeriod($dates, $fluidbook);
- $chart_heading = sprintf(__('%s Details'), $period_map[$period]['periodic']);
+ $chart_heading = __('Détails :period', ['period' => $period_map[$period]['periodic']]);
$report_timespan = '';
// Which mode are we in?
$start_date = $fluidbook->created_at->isoFormat('YYYY-MM-DD');
$end_date = now()->isoFormat('YYYY-MM-DD');
$date_range = "{$start_date},{$end_date}"; // All data up until today's date
- $chart_heading = __('Overview');
+ $chart_heading = __('Aperçu');
}
$formatted_date_range = $this->formatDateRange($start_date, $end_date);
// They are converted to be keyed by the event category name, allowing easier access
// Note: this API request includes the expanded subtable data, which is only needed for the "menu" category
// because it contains the "Download" and "Print" events as children and this structure can't be changed.
- $eventsByPeriod = collect($report->getEventsByCategory(['expanded' => 1]))->map(function($item, $key) {
+ $eventsByPeriod = collect($report->getEventsByCategory(['expanded' => 1]))->map(function ($item, $key) {
return collect($item)->keyBy('label');
});
// While this could probably be calculated from the $eventsByPeriod data, it's cleaner to use the API
$eventsByPage = collect($report->getEventsByCategory(['expanded' => 1, 'period' => 'range']))
->keyBy('label')
- ->map(function($item, $key) {
+ ->map(function ($item, $key) {
// Make subtable data easier to lookup by keying them with the labels (ie. page numbers) for certain events
if (in_array($item['label'], ['zoom', 'bookmark', 'share']) && isset($item['subtable'])) {
- $item['subtable'] = collect($item['subtable'])->keyBy(function($item, $key) {
+ $item['subtable'] = collect($item['subtable'])->keyBy(function ($item, $key) {
// Since there's some inconsistency in the way labels are stored (some have "page x" instead
// of just the number), we strip out any non-numeric values when making the keys
return preg_replace('~\D~', '', $item['label']);
});
}
return $item;
- });
+ });
//=== PER-PAGE STATISTICS
// Fetch stats for pages, separated by their URLs.
// Since we're interested in per-page not per-period stats, we fetch the report flattened for the full date range
$pageUrls = collect($report->getPageUrls(['period' => 'range', 'flat' => 1]))
->keyBy('label')
- ->reject(function($value, $key) {
+ ->reject(function ($value, $key) {
// Remove the '/' URL because it's not a real page (it's created by the "Open Fluidbook"
// event and a separate, "real" page view is recorded at the same time)
return $key === '/';
$page_group = $page_number;
$pages[$page_group] = [
- 'page_group' => $page_group,
- 'page_number' => $page_number, // Used by table column sorter
- 'nb_visits' => $pageUrls["/page/$page_number"]['nb_visits'] ?? 0,
+ 'page_group' => $page_group,
+ 'page_number' => $page_number, // Used by table column sorter
+ 'nb_visits' => $pageUrls["/page/$page_number"]['nb_visits'] ?? 0,
'nb_uniq_visitors' => $pageUrls["/page/$page_number"]['sum_daily_nb_uniq_visitors'] ?? 0,
- 'nb_pageviews' => $pageUrls["/page/$page_number"]['nb_hits'] ?? 0,
- 'nb_zooms' => data_get($eventsByPage, "zoom.subtable.$page_number.nb_events", 0),
- 'nb_bookmarks' => data_get($eventsByPage, "bookmark.subtable.$page_number.nb_events", 0),
- 'nb_shares' => data_get($eventsByPage, "share.subtable.$page_number.nb_events", 0),
+ 'nb_pageviews' => $pageUrls["/page/$page_number"]['nb_hits'] ?? 0,
+ 'nb_zooms' => data_get($eventsByPage, "zoom.subtable.$page_number.nb_events", 0),
+ 'nb_bookmarks' => data_get($eventsByPage, "bookmark.subtable.$page_number.nb_events", 0),
+ 'nb_shares' => data_get($eventsByPage, "share.subtable.$page_number.nb_events", 0),
];
// Special case: the first page of double-page Fluidbooks is tracked as "page 0", meaning that
} elseif ($page_number % 2 === 0) {
// Only stats for even pages are considered when grouped (except for bookmarks / shares)
$following_page = $page_number + 1; // By logic, there should always be a following page
- $page_group = $page_number .'—'. $following_page;
+ $page_group = $page_number . '—' . $following_page;
$pages[$page_group] = [
- 'page_group' => $page_group,
- 'page_number' => $page_number, // Used by table column sorter
- 'nb_visits' => $pageUrls["/page/$page_number"]['nb_visits'] ?? 0,
+ 'page_group' => $page_group,
+ 'page_number' => $page_number, // Used by table column sorter
+ 'nb_visits' => $pageUrls["/page/$page_number"]['nb_visits'] ?? 0,
'nb_uniq_visitors' => $pageUrls["/page/$page_number"]['sum_daily_nb_uniq_visitors'] ?? 0,
- 'nb_pageviews' => $pageUrls["/page/$page_number"]['nb_hits'] ?? 0,
- 'nb_zooms' => data_get($eventsByPage, "zoom.subtable.$page_number.nb_events", 0),
+ 'nb_pageviews' => $pageUrls["/page/$page_number"]['nb_hits'] ?? 0,
+ 'nb_zooms' => data_get($eventsByPage, "zoom.subtable.$page_number.nb_events", 0),
// Bookmarks and shares are counted and summed for both pages in the spread
- 'nb_bookmarks' => data_get($eventsByPage, "bookmark.subtable.$page_number.nb_events", 0)
- + data_get($eventsByPage, "bookmark.subtable.$following_page.nb_events", 0),
- 'nb_shares' => data_get($eventsByPage, "share.subtable.$page_number.nb_events", 0)
- + data_get($eventsByPage, "share.subtable.$following_page.nb_events", 0),
+ 'nb_bookmarks' => data_get($eventsByPage, "bookmark.subtable.$page_number.nb_events", 0)
+ + data_get($eventsByPage, "bookmark.subtable.$following_page.nb_events", 0),
+ 'nb_shares' => data_get($eventsByPage, "share.subtable.$page_number.nb_events", 0)
+ + data_get($eventsByPage, "share.subtable.$following_page.nb_events", 0),
];
}
}
$pagesByPeriod = collect($report->getPageUrls(['expanded' => $expanded_stats]))
->map(function ($item, $date) use ($period, $fluidbook_id, $hash, $eventsByPeriod, $new_stats_cutoff) {
- if (empty($item)) {
- return $item; // Some periods might have no data
- }
+ if (empty($item)) {
+ return $item; // Some periods might have no data
+ }
- // Key results by their label to make it easier to isolate the "page" stats
- // More specifically, we want to get rid of the "/index" data because these aren't true page views
- $labelled = collect($item)->keyBy('label');
+ // Key results by their label to make it easier to isolate the "page" stats
+ // More specifically, we want to get rid of the "/index" data because these aren't true page views
+ $labelled = collect($item)->keyBy('label');
- if (!isset($labelled['page'])) {
- // If there's are no page stats, we treat it is if there's no data for this period
- // This is necessary because it's possible that there will be an '/index' item but no 'page'
- return [];
- }
+ if (!isset($labelled['page'])) {
+ // If there's are no page stats, we treat it is if there's no data for this period
+ // This is necessary because it's possible that there will be an '/index' item but no 'page'
+ return [];
+ }
- // By returning the page data directly (if available), it makes
- // the returned data flatter and easier to process later for sums etc.
- $data = $labelled['page'];
-
- //== Unique Visitors
- // Matomo doesn't provide an aggregate of unique visitors, so we must do the sum ourselves.
- // If the period is "day", the number of unique visitors will be in the key 'nb_uniq_visitors'
- // but if it is a longer period (week, month, year) then the key becomes 'sum_daily_nb_uniq_visitors'
- if($fluidbook_id >= $new_stats_cutoff) {
- $unique_visitors_key = $period === 'day' ? 'nb_uniq_visitors' : 'sum_daily_nb_uniq_visitors';
- $subpages = collect($data['subtable'] ?? []);
- $data['nb_uniq_visitors'] = $subpages->sum($unique_visitors_key);
- }
+ // By returning the page data directly (if available), it makes
+ // the returned data flatter and easier to process later for sums etc.
+ $data = $labelled['page'];
+
+ //== Unique Visitors
+ // Matomo doesn't provide an aggregate of unique visitors, so we must do the sum ourselves.
+ // If the period is "day", the number of unique visitors will be in the key 'nb_uniq_visitors'
+ // but if it is a longer period (week, month, year) then the key becomes 'sum_daily_nb_uniq_visitors'
+ if ($fluidbook_id >= $new_stats_cutoff) {
+ $unique_visitors_key = $period === 'day' ? 'nb_uniq_visitors' : 'sum_daily_nb_uniq_visitors';
+ $subpages = collect($data['subtable'] ?? []);
+ $data['nb_uniq_visitors'] = $subpages->sum($unique_visitors_key);
+ }
- $data['raw_date'] = $date; // We still need the raw date for sorting and other formatting purposes
+ $data['raw_date'] = $date; // We still need the raw date for sorting and other formatting purposes
- // Formatting of date changes depending on the period
- $formatted_date = $this->formatDateForPeriod($date, $period);
+ // Formatting of date changes depending on the period
+ $formatted_date = $this->formatDateForPeriod($date, $period);
- // When segregated by month or year, the periods become links
- if (in_array($period, ['month', 'year'])) {
- // Generate URL using the named route
- $URL = route('stats', compact('fluidbook_id', 'hash', 'date'));
- $data['formatted_date'] = "<a href='{$URL}'>{$formatted_date}</a>";
- } else {
- $data['formatted_date'] = $formatted_date;
- }
+ // When segregated by month or year, the periods become links
+ if (in_array($period, ['month', 'year'])) {
+ // Generate URL using the named route
+ $URL = route('stats', compact('fluidbook_id', 'hash', 'date'));
+ $data['formatted_date'] = "<a href='{$URL}'>{$formatted_date}</a>";
+ } else {
+ $data['formatted_date'] = $formatted_date;
+ }
- // Add certain event category data
- $period_events = $eventsByPeriod->get($date);
- $data['nb_zooms'] = data_get($period_events, 'zoom.nb_events', 0);
- $data['nb_shares'] = data_get($period_events, 'share.nb_events', 0);
- $data['nb_links'] = data_get($period_events, 'link.nb_events', 0); // Used instead of standard outlinks count
+ // Add certain event category data
+ $period_events = $eventsByPeriod->get($date);
+ $data['nb_zooms'] = data_get($period_events, 'zoom.nb_events', 0);
+ $data['nb_shares'] = data_get($period_events, 'share.nb_events', 0);
+ $data['nb_links'] = data_get($period_events, 'link.nb_events', 0); // Used instead of standard outlinks count
- // Download and Print events are stored differently and can't be changed now
- // (see https://redmine.cubedesigners.com/issues/5431#note-2)
- // If 'menu' event exists, collect and key it by label so we can use data_get() for cleaner access
- $menu_events = $period_events->has('menu') ? collect($period_events['menu']['subtable'])->keyBy('label') : [];
- $data['nb_downloads'] = data_get($menu_events, 'download.nb_events', 0);
- $data['nb_prints'] = data_get($menu_events, 'print.nb_events', 0);
+ // Download and Print events are stored differently and can't be changed now
+ // (see https://redmine.cubedesigners.com/issues/5431#note-2)
+ // If 'menu' event exists, collect and key it by label so we can use data_get() for cleaner access
+ $menu_events = $period_events->has('menu') ? collect($period_events['menu']['subtable'])->keyBy('label') : [];
+ $data['nb_downloads'] = data_get($menu_events, 'download.nb_events', 0);
+ $data['nb_prints'] = data_get($menu_events, 'print.nb_events', 0);
- return $data;
- });
+ return $data;
+ });
//=== SUMMARY BY PERIOD
// Generate a list of available periods, based on the visits API data.
// If there are no visits for a certain period, we can assume that there won't be any data for other metrics.
// The goal is to find the non-empty results and create a list of Carbon date classes from those
// This list is used to generate the links / formatted dates list + detailed data table
- $period_details = $pagesByPeriod->filter(fn ($value, $key) => !empty($value)); // Remove any empty periods
+ $period_details = $pagesByPeriod->filter(fn($value, $key) => !empty($value)); // Remove any empty periods
//=== CHART PREPARATION
// Format dates for display as labels on the x-axis and in the tooltips / tables
- $tooltip_labels = $pagesByPeriod->keys()->mapWithKeys(function($date, $index) use ($period, $start_date, $end_date) {
+ $tooltip_labels = $pagesByPeriod->keys()->mapWithKeys(function ($date, $index) use ($period, $start_date, $end_date) {
$short_label = $this->formatDateForXAxis($date, $period, $start_date, $end_date);
$full_label = $this->formatDateForPeriod($date, $period);
return [$short_label => $full_label];
$chart_datasets = [
[
- 'label' => __('Visits'),
+ 'label' => __('Visites'),
'backgroundColor' => 'hsl(72 100% 38% / 100%)', // Could make bars semi-transparent if desired
// borderColor: 'hsl(72 100% 38%)',
// borderWidth: 1,
'bar_offset' => -5, // Negative offset shifts bar to left
],
[
- 'label' => __('Page Views'),
+ 'label' => __('Pages vues'),
'backgroundColor' => 'hsl(0 0% 53%)',
'data' => $pagesByPeriod->pluck('nb_hits')->toArray(),
'order' => 2,
// Insert the unique visitors dataset at the beginning of the array
$chart_datasets = array_merge([
[
- 'label' => __('Unique Visitors'),
+ 'label' => __('Visiteurs uniques'),
'backgroundColor' => 'hsl(19 100% 48% / 100%)',
'data' => $pagesByPeriod->pluck('nb_uniq_visitors')->toArray(),
'order' => 1,
$table_map = [
// Main summary table
'summary' => [
- 'formatted_date' => $period_map[$period]['singular'],
- 'nb_uniq_visitors' => __('Unique Visitors'),
- 'nb_visits' => __('Visits'),
- 'nb_hits' => __('Pages Viewed'),
- 'nb_links' => __('Outgoing Links'),
- 'nb_downloads' => __('Downloads'),
- 'nb_prints' => __('Prints'),
- 'nb_zooms' => __('Zooms'),
+ 'formatted_date' => $period_map[$period]['singular'],
+ 'nb_uniq_visitors' => __('Visiteurs uniques'),
+ 'nb_visits' => __('Visites'),
+ 'nb_hits' => __('Pages vues'),
+ 'nb_links' => __('Liens sortants'),
+ 'nb_downloads' => __('Téléchargements'),
+ 'nb_prints' => __('Impressions'),
+ 'nb_zooms' => __('Zooms'),
],
// Per-page detail table
'per-page' => [
- 'page_group' => __('Pages'),
- 'nb_uniq_visitors' => __('Unique Visits'),
- 'nb_visits' => __('Visits'),
- 'nb_pageviews' => __('Views'),
- 'nb_zooms' => __('Zooms'),
- 'nb_bookmarks' => __('Bookmarks'),
- 'nb_shares' => __('Shares'),
+ 'page_group' => __('Pages'),
+ 'nb_uniq_visitors' => __('Visites uniques'),
+ 'nb_visits' => __('Visites'),
+ 'nb_pageviews' => __('Vues'),
+ 'nb_zooms' => __('Zooms'),
+ 'nb_bookmarks' => __('Pages marquées'),
+ 'nb_shares' => __('Partages'),
],
];
// This was added as a convenience when quickly switching between stats views for Fluidbooks
// If the current user has sufficient permissions, they will be redirected to the hashed URL.
// If not, the FluidbookPublication lookup will fail and further execution will halt.
- protected function statsRedirect($fluidbook_id) {
+ protected function statsRedirect($fluidbook_id)
+ {
$fluidbook = FluidbookPublication::where('id', $fluidbook_id)->firstOrFail();
$settings = json_decode($fluidbook->settings);
}
// https://toolbox.fluidbook.com/fluidbook-publication/stats/API
- protected function statsAPI() {
+ protected function statsAPI()
+ {
if (!can('superadmin')) {
// Only allow superadmin access because this is a dev tool, and it exposes the API tokens
}
// Format a date range for display in the interface
- protected function formatDateRange($start_date, $end_date): string {
+ protected function formatDateRange($start_date, $end_date): string
+ {
$start = Carbon::parse($start_date);
$end = Carbon::parse($end_date);
// Format dates depending on the segregation period (used for tooltips and stats detail tables)
- protected function formatDateForPeriod($date, $period): string {
+ protected function formatDateForPeriod($date, $period): string
+ {
// Weeks are a special case because they contain two dates
if ($period === 'week') {
$crosses_month_boundary = $week_end->isoFormat('YYYY-MM') > $week_start->isoFormat('YYYY-MM');
$crosses_year_boundary = $week_end->isoFormat('YYYY') > $week_start->isoFormat('YYYY');
if ($crosses_year_boundary) {
- $week_formatted = $week_start->isoFormat('Do MMMM, YYYY') .' — '. $week_end->isoFormat('Do MMMM, YYYY');
+ $week_formatted = $week_start->isoFormat('Do MMMM, YYYY') . ' — ' . $week_end->isoFormat('Do MMMM, YYYY');
} elseif ($crosses_month_boundary) {
- $week_formatted = $week_start->isoFormat('Do MMMM') .' — '. $week_end->isoFormat('Do MMMM, YYYY');
+ $week_formatted = $week_start->isoFormat('Do MMMM') . ' — ' . $week_end->isoFormat('Do MMMM, YYYY');
} else {
- $week_formatted = $week_start->isoFormat('Do') .'—'. $week_end->isoFormat('Do MMMM, YYYY');
+ $week_formatted = $week_start->isoFormat('Do') . '—' . $week_end->isoFormat('Do MMMM, YYYY');
}
}
}
// Dates displayed in the x-axis of chart need different formatting
- protected function formatDateForXAxis($date, $period, $start_date, $end_date): string {
+ protected function formatDateForXAxis($date, $period, $start_date, $end_date): string
+ {
// The labels on the x-axis should be as concise as possible but when the date range crosses
// a boundary for the larger period (eg. period = day but the range crosses more than one month),
$crosses_month_boundary = $week_end->isoFormat('YYYY-MM') > $week_start->isoFormat('YYYY-MM');
$crosses_year_boundary = $week_end->isoFormat('YYYY') > $week_start->isoFormat('YYYY');
if ($crosses_year_boundary) {
- $week_formatted = $week_start->isoFormat('D MMM YYYY') .' — '. $week_end->isoFormat('D MMM YYYY');
+ $week_formatted = $week_start->isoFormat('D MMM YYYY') . ' — ' . $week_end->isoFormat('D MMM YYYY');
} elseif ($crosses_month_boundary) {
- $week_formatted = $week_start->isoFormat('D MMM') .' — '. $week_end->isoFormat('D MMM');
+ $week_formatted = $week_start->isoFormat('D MMM') . ' — ' . $week_end->isoFormat('D MMM');
} else {
- $week_formatted = $week_start->isoFormat('D') .'—'. $week_end->isoFormat('D MMM');
+ $week_formatted = $week_start->isoFormat('D') . '—' . $week_end->isoFormat('D MMM');
}
} elseif ($period === 'year') {
// Years also need special handling because if we try to use Carbon::parse()
return match ($period) {
'day' => $crosses_year_boundary ? $date->isoFormat('DD MMM YYYY')
- : ($crosses_month_boundary ? $date->isoFormat('DD MMM') : $date->isoFormat('DD')),
+ : ($crosses_month_boundary ? $date->isoFormat('DD MMM') : $date->isoFormat('DD')),
'week' => $week_formatted,
'month' => $crosses_year_boundary ? $date->isoFormat('MMM YYYY') : $date->isoFormat('MMM'),
default => $date,