Changeset 3798238
- Timestamp:
- 11/12/2026 11:56:22 AM (45 hours ago)
- Location:
- headless-ga4
- Files:
-
- 16 edited
- 1 copied
-
tags/0.2.1 (copied) (copied from headless-ga4/trunk )
-
tags/0.2.1/assets/admin.css (modified) (6 diffs)
-
tags/0.2.1/headless-ga4.php (modified) (2 diffs)
-
tags/0.2.1/includes/cache.php (modified) (3 diffs)
-
tags/0.2.1/includes/dashboard-widget.php (modified) (8 diffs)
-
tags/0.2.1/includes/ga4-api.php (modified) (13 diffs)
-
tags/0.2.1/includes/settings-page.php (modified) (3 diffs)
-
tags/0.2.1/readme.txt (modified) (3 diffs)
-
tags/0.2.1/templates/dashboard-widget.php (modified) (7 diffs)
-
trunk/assets/admin.css (modified) (6 diffs)
-
trunk/headless-ga4.php (modified) (2 diffs)
-
trunk/includes/cache.php (modified) (3 diffs)
-
trunk/includes/dashboard-widget.php (modified) (8 diffs)
-
trunk/includes/ga4-api.php (modified) (13 diffs)
-
trunk/includes/settings-page.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/templates/dashboard-widget.php (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
headless-ga4/tags/0.2.1/assets/admin.css
r3504948 r3798238 73 73 } 74 74 75 .headless-ga4-widget-period-header { 76 display: flex; 77 align-items: center; 78 justify-content: space-between; 79 gap: 14px; 80 margin-bottom: 16px; 81 } 82 83 .headless-ga4-widget-period-header strong { 84 display: block; 85 margin-top: 4px; 86 font-size: 18px; 87 } 88 89 .headless-ga4-widget-period-filter { 90 display: flex; 91 flex-wrap: wrap; 92 gap: 6px; 93 } 94 95 .headless-ga4-widget-period-link { 96 border: 1px solid #c3c4c7; 97 border-radius: 999px; 98 background: #f6f7f7; 99 color: #1d2327; 100 padding: 5px 10px; 101 text-decoration: none; 102 } 103 104 .headless-ga4-widget-period-link:hover, 105 .headless-ga4-widget-period-link:focus { 106 border-color: #2271b1; 107 color: #0a4b78; 108 } 109 110 .headless-ga4-widget-period-link.is-active { 111 border-color: #1d2327; 112 background: #1d2327; 113 color: #fff; 114 } 115 75 116 .headless-ga4-widget-card, 76 117 .headless-ga4-widget-panel { … … 109 150 .headless-ga4-widget-list { 110 151 margin: 10px 0 0; 152 max-height: 230px; 153 overflow: auto; 111 154 } 112 155 … … 128 171 margin: 0; 129 172 font-size: 14px; 173 } 174 175 .headless-ga4-widget-rank-bar { 176 display: block; 177 width: min(180px, 100%); 178 height: 10px; 179 margin-top: 8px; 180 } 181 182 .headless-ga4-widget-rank-bar-track { 183 fill: #edf0f2; 184 } 185 186 .headless-ga4-widget-rank-bar-value { 187 fill: #2271b1; 130 188 } 131 189 … … 145 203 .headless-ga4-widget-chart { 146 204 margin-top: 10px; 205 border: 1px solid #ececec; 206 background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%); 207 padding: 10px; 147 208 } 148 209 … … 151 212 width: 100%; 152 213 height: auto; 214 } 215 216 .headless-ga4-widget-chart-bars rect { 217 fill: #d7e8f7; 218 } 219 220 .headless-ga4-widget-chart-line { 221 stroke: #111111; 222 stroke-linecap: round; 223 stroke-linejoin: round; 224 stroke-width: 3; 153 225 } 154 226 … … 170 242 171 243 @media (max-width: 782px) { 244 .headless-ga4-widget-period-header { 245 align-items: flex-start; 246 flex-direction: column; 247 } 248 172 249 .headless-ga4-widget-grid, 173 250 .headless-ga4-widget-layout { -
headless-ga4/tags/0.2.1/headless-ga4.php
r3504948 r3798238 4 4 * Plugin URI: https://studio.jakubkanna.com/ 5 5 * Description: Display lightweight GA4 data in the WordPress dashboard for headless frontends. 6 * Version: 0. 1.16 * Version: 0.2.1 7 7 * Requires at least: 6.4 8 8 * Requires PHP: 7.4 … … 19 19 } 20 20 21 define('HEADLESS_GA4_VERSION', '0. 1.1');21 define('HEADLESS_GA4_VERSION', '0.2.1'); 22 22 define('HEADLESS_GA4_PLUGIN_FILE', __FILE__); 23 23 define('HEADLESS_GA4_PLUGIN_PATH', plugin_dir_path(__FILE__)); -
headless-ga4/tags/0.2.1/includes/cache.php
r3504948 r3798238 55 55 56 56 /** 57 * Returns the transient name for a period-specific report cache. 58 * 59 * @param string $period Dashboard period key. 60 * 61 * @return string 62 */ 63 function headless_ga4_get_report_transient_name($period = 'weekly') 64 { 65 $period = is_string($period) ? sanitize_key($period) : 'weekly'; 66 67 if (!in_array($period, array('weekly', 'monthly', 'yearly', 'all_time'), true)) { 68 $period = 'weekly'; 69 } 70 71 return HEADLESS_GA4_REPORT_TRANSIENT . '_' . $period; 72 } 73 74 /** 57 75 * Returns the cached report payload. 76 * 77 * @param string $period Dashboard period key. 58 78 * 59 79 * @return array<string, mixed>|null 60 80 */ 61 function headless_ga4_get_cached_report( )81 function headless_ga4_get_cached_report($period = 'weekly') 62 82 { 63 $cached = get_transient( HEADLESS_GA4_REPORT_TRANSIENT);83 $cached = get_transient(headless_ga4_get_report_transient_name($period)); 64 84 65 85 return is_array($cached) ? $cached : null; … … 70 90 * 71 91 * @param array<string, mixed> $report Report payload. 92 * @param string $period Dashboard period key. 72 93 * 73 94 * @return void 74 95 */ 75 function headless_ga4_set_cached_report($report )96 function headless_ga4_set_cached_report($report, $period = 'weekly') 76 97 { 77 98 set_transient( 78 HEADLESS_GA4_REPORT_TRANSIENT,99 headless_ga4_get_report_transient_name($period), 79 100 $report, 80 101 headless_ga4_get_cache_ttl_seconds() … … 89 110 function headless_ga4_clear_cached_data() 90 111 { 112 $periods = array('weekly', 'monthly', 'yearly', 'all_time'); 113 91 114 delete_transient(HEADLESS_GA4_ACCESS_TOKEN_TRANSIENT); 92 115 delete_transient(HEADLESS_GA4_REPORT_TRANSIENT); 116 117 foreach ($periods as $period) { 118 delete_transient(headless_ga4_get_report_transient_name($period)); 119 } 93 120 } 94 -
headless-ga4/tags/0.2.1/includes/dashboard-widget.php
r3504948 r3798238 43 43 44 44 /** 45 * Builds a simple SVG polyline chart for the sessions trend. 46 * 47 * @param array<int, array<string, mixed>> $trend Trend rows. 48 * 49 * @return string 50 */ 51 function headless_ga4_render_trend_svg($trend) 45 * Returns the dashboard period requested in the URL. 46 * 47 * @return string 48 */ 49 function headless_ga4_get_requested_dashboard_period() 50 { 51 $period = filter_input(INPUT_GET, 'headless_ga4_period', FILTER_SANITIZE_SPECIAL_CHARS); 52 53 return headless_ga4_normalize_dashboard_period(is_string($period) ? $period : 'weekly'); 54 } 55 56 /** 57 * Returns the dashboard period posted by the refresh form. 58 * 59 * @return string 60 */ 61 function headless_ga4_get_posted_dashboard_period() 62 { 63 $period = filter_input(INPUT_POST, 'headless_ga4_period', FILTER_SANITIZE_SPECIAL_CHARS); 64 65 return headless_ga4_normalize_dashboard_period(is_string($period) ? $period : 'weekly'); 66 } 67 68 /** 69 * Returns allowed SVG markup for dashboard charts. 70 * 71 * @return array<string, array<string, bool>> 72 */ 73 function headless_ga4_get_allowed_chart_svg_html() 74 { 75 return array( 76 'svg' => array( 77 'aria-label' => true, 78 'class' => true, 79 'role' => true, 80 'viewbox' => true, 81 'viewBox' => true, 82 ), 83 'g' => array( 84 'class' => true, 85 ), 86 'polyline' => array( 87 'class' => true, 88 'fill' => true, 89 'points' => true, 90 ), 91 'rect' => array( 92 'class' => true, 93 'height' => true, 94 'rx' => true, 95 'width' => true, 96 'x' => true, 97 'y' => true, 98 ), 99 ); 100 } 101 102 /** 103 * Builds an SVG bar and line diagram for the sessions trend. 104 * 105 * @param array<int, array<string, mixed>> $trend Trend rows. 106 * @param string $chart_label Accessible chart label. 107 * 108 * @return string 109 */ 110 function headless_ga4_render_trend_svg($trend, $chart_label = '') 52 111 { 53 112 if (empty($trend)) { … … 55 114 } 56 115 57 $width = 320; 58 $height = 92; 59 $padding = 8; 116 $width = 520; 117 $height = 180; 118 $padding_x = 18; 119 $padding_y = 16; 60 120 $values = array_map( 61 121 static function ($point) { … … 65 125 ); 66 126 $max_value = max(1, max($values)); 67 $min_value = min($values); 68 $range = max(1, $max_value - $min_value); 127 $range = max(1, $max_value); 69 128 $count = count($trend); 70 $step_x = $count > 1 ? ($width - ($padding * 2)) / ($count - 1) : 0; 129 $inner_width = $width - ($padding_x * 2); 130 $inner_height = $height - ($padding_y * 2); 131 $step_x = $count > 1 ? $inner_width / ($count - 1) : 0; 132 $bar_slot = $count > 0 ? $inner_width / $count : $inner_width; 133 $bar_width = max(3, min(22, $bar_slot * 0.55)); 71 134 $points = array(); 135 $bars = array(); 136 $chart_label = $chart_label !== '' ? $chart_label : __('Sessions trend', 'headless-ga4'); 72 137 73 138 foreach ($trend as $index => $point) { 74 139 $value = isset($point['value']) ? (int) $point['value'] : 0; 75 $x = $padding + ($step_x * $index); 76 $y = $height - $padding - ((($value - $min_value) / $range) * ($height - ($padding * 2))); 140 $x = $padding_x + ($step_x * $index); 141 $bar_height = max(1, ($value / $range) * $inner_height); 142 $bar_x = $x - ($bar_width / 2); 143 $bar_y = $height - $padding_y - $bar_height; 144 $y = $height - $padding_y - (($value / $range) * $inner_height); 77 145 $points[] = round($x, 2) . ',' . round($y, 2); 78 } 79 80 return '<svg viewBox="0 0 ' . esc_attr((string) $width) . ' ' . esc_attr((string) $height) . '" role="img" aria-label="' . esc_attr__('Sessions trend for the last 7 days', 'headless-ga4') . '"><polyline fill="none" stroke="#111111" stroke-width="2" points="' . esc_attr(implode(' ', $points)) . '" /></svg>'; 146 $bars[] = '<rect x="' . esc_attr((string) round($bar_x, 2)) . '" y="' . esc_attr((string) round($bar_y, 2)) . '" width="' . esc_attr((string) round($bar_width, 2)) . '" height="' . esc_attr((string) round($bar_height, 2)) . '" rx="3" />'; 147 } 148 149 return '<svg viewBox="0 0 ' . esc_attr((string) $width) . ' ' . esc_attr((string) $height) . '" role="img" aria-label="' . esc_attr($chart_label) . '"><g class="headless-ga4-widget-chart-bars">' . implode('', $bars) . '</g><polyline class="headless-ga4-widget-chart-line" fill="none" points="' . esc_attr(implode(' ', $points)) . '" /></svg>'; 150 } 151 152 /** 153 * Builds a compact horizontal bar SVG for ranked metric rows. 154 * 155 * @param int $value Metric value. 156 * @param int $max_value Largest value in the list. 157 * @param string $label Accessible chart label. 158 * 159 * @return string 160 */ 161 function headless_ga4_render_horizontal_bar_svg($value, $max_value, $label) 162 { 163 $value = max(0, (int) $value); 164 $max_value = max(1, (int) $max_value); 165 $width = 180; 166 $height = 10; 167 $bar_width = max(1, min($width, ($value / $max_value) * $width)); 168 169 return '<svg class="headless-ga4-widget-rank-bar" viewBox="0 0 ' . esc_attr((string) $width) . ' ' . esc_attr((string) $height) . '" role="img" aria-label="' . esc_attr($label) . '"><rect class="headless-ga4-widget-rank-bar-track" x="0" y="0" width="' . esc_attr((string) $width) . '" height="' . esc_attr((string) $height) . '" rx="5" /><rect class="headless-ga4-widget-rank-bar-value" x="0" y="0" width="' . esc_attr((string) round($bar_width, 2)) . '" height="' . esc_attr((string) $height) . '" rx="5" /></svg>'; 81 170 } 82 171 … … 93 182 94 183 check_admin_referer('headless_ga4_refresh'); 184 $period = headless_ga4_get_posted_dashboard_period(); 95 185 headless_ga4_clear_cached_data(); 96 $report = headless_ga4_fetch_dashboard_report(true );186 $report = headless_ga4_fetch_dashboard_report(true, $period); 97 187 98 188 if (is_wp_error($report)) { … … 102 192 } 103 193 104 wp_safe_redirect(ad min_url('index.php'));194 wp_safe_redirect(add_query_arg('headless_ga4_period', $period, admin_url('index.php'))); 105 195 exit; 106 196 } … … 122 212 $top_sources = array(); 123 213 $last_refreshed_at = 0; 214 $current_period = headless_ga4_get_requested_dashboard_period(); 215 $period_choices = headless_ga4_get_dashboard_period_choices(); 216 $period_label = isset($period_choices[$current_period]) ? $period_choices[$current_period] : __('Weekly', 'headless-ga4'); 217 $trend_label = __('Daily sessions', 'headless-ga4'); 218 $headless_ga4_top_pages_max_views = 0; 219 $headless_ga4_top_sources_max_sessions = 0; 124 220 $cards = array( 125 221 'users' => __('Users', 'headless-ga4'), … … 135 231 } 136 232 137 $report = headless_ga4_fetch_dashboard_report( );233 $report = headless_ga4_fetch_dashboard_report(false, $current_period); 138 234 139 235 if (is_wp_error($report)) { … … 150 246 $top_sources = isset($report['top_sources']) && is_array($report['top_sources']) ? $report['top_sources'] : array(); 151 247 $last_refreshed_at = isset($report['last_refreshed_at']) ? (int) $report['last_refreshed_at'] : 0; 248 $trend_label = isset($report['trend_label']) ? (string) $report['trend_label'] : $trend_label; 249 $period_label = isset($report['period_label']) ? (string) $report['period_label'] : $period_label; 250 251 foreach ($top_pages as $headless_ga4_top_page) { 252 $headless_ga4_top_pages_max_views = max( 253 $headless_ga4_top_pages_max_views, 254 isset($headless_ga4_top_page['views']) ? (int) $headless_ga4_top_page['views'] : 0 255 ); 256 } 257 258 foreach ($top_sources as $headless_ga4_top_source) { 259 $headless_ga4_top_sources_max_sessions = max( 260 $headless_ga4_top_sources_max_sessions, 261 isset($headless_ga4_top_source['sessions']) ? (int) $headless_ga4_top_source['sessions'] : 0 262 ); 263 } 152 264 153 265 require HEADLESS_GA4_PLUGIN_PATH . 'templates/dashboard-widget.php'; -
headless-ga4/tags/0.2.1/includes/ga4-api.php
r3504948 r3798238 118 118 119 119 /** 120 * Returns the available dashboard reporting periods. 121 * 122 * @return array<string, string> 123 */ 124 function headless_ga4_get_dashboard_period_choices() 125 { 126 return array( 127 'weekly' => __('Weekly', 'headless-ga4'), 128 'monthly' => __('Monthly', 'headless-ga4'), 129 'yearly' => __('Yearly', 'headless-ga4'), 130 'all_time' => __('All time', 'headless-ga4'), 131 ); 132 } 133 134 /** 135 * Normalizes the dashboard period key. 136 * 137 * @param string $period Raw period key. 138 * 139 * @return string 140 */ 141 function headless_ga4_normalize_dashboard_period($period) 142 { 143 $period = is_string($period) ? sanitize_key($period) : 'weekly'; 144 $choices = headless_ga4_get_dashboard_period_choices(); 145 146 return isset($choices[$period]) ? $period : 'weekly'; 147 } 148 149 /** 150 * Returns report configuration for a dashboard period. 151 * 152 * @param string $period Dashboard period key. 153 * 154 * @return array<string, mixed> 155 */ 156 function headless_ga4_get_dashboard_period_config($period) 157 { 158 $period = headless_ga4_normalize_dashboard_period($period); 159 $choices = headless_ga4_get_dashboard_period_choices(); 160 $configs = array( 161 'weekly' => array( 162 'current_range' => array('startDate' => '6daysAgo', 'endDate' => 'today'), 163 'previous_range' => array('startDate' => '13daysAgo', 'endDate' => '7daysAgo'), 164 'trend_dimension' => 'date', 165 'trend_label' => __('Daily sessions', 'headless-ga4'), 166 'trend_limit' => 7, 167 ), 168 'monthly' => array( 169 'current_range' => array('startDate' => '29daysAgo', 'endDate' => 'today'), 170 'previous_range' => array('startDate' => '59daysAgo', 'endDate' => '30daysAgo'), 171 'trend_dimension' => 'date', 172 'trend_label' => __('Daily sessions', 'headless-ga4'), 173 'trend_limit' => 30, 174 ), 175 'yearly' => array( 176 'current_range' => array('startDate' => '365daysAgo', 'endDate' => 'today'), 177 'previous_range' => array('startDate' => '731daysAgo', 'endDate' => '366daysAgo'), 178 'trend_dimension' => 'yearMonth', 179 'trend_label' => __('Monthly sessions', 'headless-ga4'), 180 'trend_limit' => 13, 181 ), 182 'all_time' => array( 183 'current_range' => array('startDate' => '2020-10-14', 'endDate' => 'today'), 184 'previous_range' => null, 185 'trend_dimension' => 'year', 186 'trend_label' => __('Yearly sessions', 'headless-ga4'), 187 'trend_limit' => 100, 188 ), 189 ); 190 191 $config = $configs[$period]; 192 $config['key'] = $period; 193 $config['label'] = $choices[$period]; 194 195 return $config; 196 } 197 198 /** 199 * Formats a trend dimension value for display. 200 * 201 * @param string $value Raw GA4 dimension value. 202 * @param string $dimension GA4 dimension name. 203 * 204 * @return string 205 */ 206 function headless_ga4_format_trend_label($value, $dimension) 207 { 208 if ($dimension === 'date') { 209 $date = DateTime::createFromFormat('Ymd', $value, wp_timezone()); 210 211 return $date instanceof DateTime ? wp_date('M j', $date->getTimestamp()) : $value; 212 } 213 214 if ($dimension === 'yearMonth') { 215 $date = DateTime::createFromFormat('Ym', $value, wp_timezone()); 216 217 return $date instanceof DateTime ? wp_date('M Y', $date->getTimestamp()) : $value; 218 } 219 220 return $value; 221 } 222 223 /** 224 * Returns an empty metric summary. 225 * 226 * @return array<string, int> 227 */ 228 function headless_ga4_get_empty_metric_summary() 229 { 230 return array( 231 'activeUsers' => 0, 232 'sessions' => 0, 233 'screenPageViews' => 0, 234 'engagedSessions' => 0, 235 ); 236 } 237 238 /** 239 * Builds normalized trend points and fills empty buckets for the selected period. 240 * 241 * @param array<string, mixed> $trend_report Trend report. 242 * @param array<string, mixed> $period_config Period configuration. 243 * 244 * @return array<int, array<string, int|string>> 245 */ 246 function headless_ga4_build_trend_points($trend_report, $period_config) 247 { 248 $trend_rows = isset($trend_report['rows']) && is_array($trend_report['rows']) ? $trend_report['rows'] : array(); 249 $trend_dimension = isset($period_config['trend_dimension']) ? (string) $period_config['trend_dimension'] : 'date'; 250 $period = isset($period_config['key']) ? (string) $period_config['key'] : 'weekly'; 251 $values = array(); 252 253 foreach ($trend_rows as $row) { 254 $dimension_value = isset($row['dimensionValues'][0]['value']) ? (string) $row['dimensionValues'][0]['value'] : ''; 255 256 if ($dimension_value === '') { 257 continue; 258 } 259 260 $values[$dimension_value] = isset($row['metricValues'][0]['value']) ? (int) $row['metricValues'][0]['value'] : 0; 261 } 262 263 if ($period === 'weekly' || $period === 'monthly') { 264 $days = $period === 'weekly' ? 7 : 30; 265 $date = new DateTime('-' . ($days - 1) . ' days', wp_timezone()); 266 $trend = array(); 267 268 for ($index = 0; $index < $days; $index++) { 269 $key = $date->format('Ymd'); 270 $trend[] = array( 271 'label' => wp_date('M j', $date->getTimestamp()), 272 'value' => isset($values[$key]) ? (int) $values[$key] : 0, 273 ); 274 $date->modify('+1 day'); 275 } 276 277 return $trend; 278 } 279 280 if ($period === 'yearly') { 281 $date = new DateTime('first day of this month', wp_timezone()); 282 $date->modify('-12 months'); 283 $trend = array(); 284 285 for ($index = 0; $index < 13; $index++) { 286 $key = $date->format('Ym'); 287 $trend[] = array( 288 'label' => wp_date('M Y', $date->getTimestamp()), 289 'value' => isset($values[$key]) ? (int) $values[$key] : 0, 290 ); 291 $date->modify('+1 month'); 292 } 293 294 return $trend; 295 } 296 297 if (empty($values)) { 298 return array( 299 array( 300 'label' => wp_date('Y'), 301 'value' => 0, 302 ), 303 ); 304 } 305 306 $trend = array(); 307 308 foreach ($values as $dimension_value => $session_value) { 309 $trend[] = array( 310 'label' => headless_ga4_format_trend_label((string) $dimension_value, $trend_dimension), 311 'value' => (int) $session_value, 312 ); 313 } 314 315 return $trend; 316 } 317 318 /** 120 319 * Builds the normalized widget payload from GA4 report responses. 121 320 * … … 125 324 * @param array<string, mixed> $pages_report Top pages report. 126 325 * @param array<string, mixed> $sources_report Top sources report. 326 * @param array<string, mixed> $period_config Period configuration. 127 327 * 128 328 * @return array<string, mixed> 129 329 */ 130 function headless_ga4_build_dashboard_payload($current_summary, $previous_summary, $trend_report, $pages_report, $sources_report )330 function headless_ga4_build_dashboard_payload($current_summary, $previous_summary, $trend_report, $pages_report, $sources_report, $period_config) 131 331 { 132 332 $summary = array( … … 168 368 ); 169 369 170 $trend = array(); 171 $trend_rows = isset($trend_report['rows']) && is_array($trend_report['rows']) ? $trend_report['rows'] : array(); 172 173 foreach ($trend_rows as $row) { 174 $date_value = isset($row['dimensionValues'][0]['value']) ? (string) $row['dimensionValues'][0]['value'] : ''; 175 $session_value = isset($row['metricValues'][0]['value']) ? (int) $row['metricValues'][0]['value'] : 0; 176 177 if ($date_value === '') { 178 continue; 179 } 180 181 $date = DateTime::createFromFormat('Ymd', $date_value, wp_timezone()); 182 183 $trend[] = array( 184 'label' => $date instanceof DateTime ? wp_date('M j', $date->getTimestamp()) : $date_value, 185 'value' => $session_value, 186 ); 187 } 370 $trend = headless_ga4_build_trend_points($trend_report, $period_config); 188 371 189 372 $top_pages = array(); … … 221 404 'comparison' => $comparison, 222 405 'trend' => $trend, 406 'period' => isset($period_config['key']) ? (string) $period_config['key'] : 'weekly', 407 'period_label' => isset($period_config['label']) ? (string) $period_config['label'] : __('Weekly', 'headless-ga4'), 408 'trend_label' => isset($period_config['trend_label']) ? (string) $period_config['trend_label'] : __('Daily sessions', 'headless-ga4'), 223 409 'top_pages' => $top_pages, 224 410 'top_sources' => $top_sources, … … 231 417 * 232 418 * @param bool $force_refresh Whether to bypass the report cache. 419 * @param string $period Dashboard period key. 233 420 * 234 421 * @return array<string, mixed>|WP_Error 235 422 */ 236 function headless_ga4_fetch_dashboard_report($force_refresh = false )423 function headless_ga4_fetch_dashboard_report($force_refresh = false, $period = 'weekly') 237 424 { 238 425 if (!headless_ga4_is_configured()) { … … 243 430 } 244 431 432 $period = headless_ga4_normalize_dashboard_period($period); 433 $period_config = headless_ga4_get_dashboard_period_config($period); 434 $current_range = isset($period_config['current_range']) && is_array($period_config['current_range']) ? $period_config['current_range'] : array('startDate' => '6daysAgo', 'endDate' => 'today'); 435 $previous_range = isset($period_config['previous_range']) && is_array($period_config['previous_range']) ? $period_config['previous_range'] : null; 436 $trend_dimension = isset($period_config['trend_dimension']) ? (string) $period_config['trend_dimension'] : 'date'; 437 $trend_limit = isset($period_config['trend_limit']) ? (int) $period_config['trend_limit'] : 30; 438 245 439 if (!$force_refresh) { 246 $cached_report = headless_ga4_get_cached_report( );440 $cached_report = headless_ga4_get_cached_report($period); 247 441 248 442 if (is_array($cached_report)) { … … 254 448 array( 255 449 'dateRanges' => array( 256 array( 257 'startDate' => '6daysAgo', 258 'endDate' => 'today', 259 ), 450 $current_range, 260 451 ), 261 452 'metrics' => array( … … 272 463 } 273 464 274 $summary_previous = headless_ga4_run_report( 275 array( 276 'dateRanges' => array( 277 array( 278 'startDate' => '13daysAgo', 279 'endDate' => '7daysAgo', 465 if (is_array($previous_range)) { 466 $summary_previous = headless_ga4_run_report( 467 array( 468 'dateRanges' => array( 469 $previous_range, 280 470 ), 281 ), 282 'metrics' => array( 283 array('name' => 'activeUsers'), 284 array('name' => 'sessions'), 285 array('name' => 'screenPageViews'), 286 array('name' => 'engagedSessions'), 287 ), 288 ) 289 ); 290 291 if (is_wp_error($summary_previous)) { 292 return $summary_previous; 471 'metrics' => array( 472 array('name' => 'activeUsers'), 473 array('name' => 'sessions'), 474 array('name' => 'screenPageViews'), 475 array('name' => 'engagedSessions'), 476 ), 477 ) 478 ); 479 480 if (is_wp_error($summary_previous)) { 481 return $summary_previous; 482 } 483 484 $previous_summary = headless_ga4_map_metric_row($summary_previous); 485 } else { 486 $previous_summary = headless_ga4_get_empty_metric_summary(); 293 487 } 294 488 … … 296 490 array( 297 491 'dateRanges' => array( 298 array( 299 'startDate' => '6daysAgo', 300 'endDate' => 'today', 301 ), 492 $current_range, 302 493 ), 303 494 'dimensions' => array( 304 array('name' => 'date'),495 array('name' => $trend_dimension), 305 496 ), 306 497 'metrics' => array( … … 310 501 array( 311 502 'dimension' => array( 312 'dimensionName' => 'date',503 'dimensionName' => $trend_dimension, 313 504 ), 314 505 ), 315 506 ), 507 'limit' => $trend_limit, 316 508 ) 317 509 ); … … 324 516 array( 325 517 'dateRanges' => array( 326 array( 327 'startDate' => '6daysAgo', 328 'endDate' => 'today', 329 ), 518 $current_range, 330 519 ), 331 520 'dimensions' => array( … … 355 544 array( 356 545 'dateRanges' => array( 357 array( 358 'startDate' => '6daysAgo', 359 'endDate' => 'today', 360 ), 546 $current_range, 361 547 ), 362 548 'dimensions' => array( … … 385 571 $report = headless_ga4_build_dashboard_payload( 386 572 headless_ga4_map_metric_row($summary_current), 387 headless_ga4_map_metric_row($summary_previous),573 $previous_summary, 388 574 $trend_report, 389 575 $pages_report, 390 $sources_report 391 ); 392 393 headless_ga4_set_cached_report($report); 576 $sources_report, 577 $period_config 578 ); 579 580 headless_ga4_set_cached_report($report, $period); 394 581 395 582 return $report; 396 583 } 397 -
headless-ga4/tags/0.2.1/includes/settings-page.php
r3504948 r3798238 116 116 117 117 if ($value === '') { 118 headless_ga4_set_option_autoload_no(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION);119 118 return $current; 120 119 } … … 134 133 135 134 headless_ga4_clear_cached_data(); 136 headless_ga4_set_option_autoload_no(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION);137 135 138 136 return (string) wp_json_encode( … … 304 302 305 303 check_admin_referer('headless_ga4_clear_credentials'); 306 update_option(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION, '');307 headless_ga4_set_option_autoload_no(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION);304 delete_option(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION); 305 add_option(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION, '', '', false); 308 306 headless_ga4_clear_cached_data(); 309 307 headless_ga4_set_admin_notice(__('Stored service account credentials were cleared.', 'headless-ga4')); -
headless-ga4/tags/0.2.1/readme.txt
r3504948 r3798238 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 0. 1.17 Stable tag: 0.2.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 121 121 == Changelog == 122 122 123 = 0.2.1 = 124 125 * Fix recursive service account JSON sanitization when saving settings 126 * Fix clearing stored service account credentials 127 128 = 0.2.0 = 129 130 * Add dashboard period filters for weekly, monthly, yearly, and all-time reports 131 * Add period-aware GA4 report caching 132 * Improve the sessions trend diagram with combined bar and line chart display 133 123 134 = 0.1.1 = 124 135 … … 137 148 == Upgrade Notice == 138 149 150 = 0.2.1 = 151 152 Fixes a critical error when saving service account JSON credentials. 153 154 = 0.2.0 = 155 156 Adds dashboard report period filters and an improved sessions diagram. 157 139 158 = 0.1.1 = 140 159 -
headless-ga4/tags/0.2.1/templates/dashboard-widget.php
r3504948 r3798238 5 5 * @var array<string, string> $cards 6 6 * @var array<string, mixed> $comparison 7 * @var string $current_period 7 8 * @var int $last_refreshed_at 9 * @var string $period_label 10 * @var array<string, string> $period_choices 11 * @var int $headless_ga4_top_pages_max_views 12 * @var int $headless_ga4_top_sources_max_sessions 8 13 * @var string $state 9 14 * @var array<int, array<string, mixed>> $top_pages … … 11 16 * @var array<string, mixed> $summary 12 17 * @var array<int, array<string, mixed>> $trend 18 * @var string $trend_label 13 19 * @var string $error_message 14 20 */ … … 42 48 endif; 43 49 ?> 50 <div class="headless-ga4-widget-period-header"> 51 <div> 52 <span class="headless-ga4-widget-label"><?php esc_html_e('Report period', 'headless-ga4'); ?></span> 53 <strong><?php echo esc_html($period_label); ?></strong> 54 </div> 55 <div class="headless-ga4-widget-period-filter" aria-label="<?php esc_attr_e('Choose report period', 'headless-ga4'); ?>"> 56 <?php foreach ($period_choices as $headless_ga4_period_key => $headless_ga4_period_choice_label) : ?> 57 <a 58 class="headless-ga4-widget-period-link<?php echo $current_period === $headless_ga4_period_key ? ' is-active' : ''; ?>" 59 href="<?php echo esc_url(add_query_arg('headless_ga4_period', $headless_ga4_period_key, admin_url('index.php'))); ?>" 60 <?php echo $current_period === $headless_ga4_period_key ? 'aria-current="true"' : ''; ?> 61 > 62 <?php echo esc_html($headless_ga4_period_choice_label); ?> 63 </a> 64 <?php endforeach; ?> 65 </div> 66 </div> 67 44 68 <div class="headless-ga4-widget-grid"> 45 69 <?php foreach ($cards as $headless_ga4_card_key => $headless_ga4_card_label) : ?> … … 59 83 <div class="headless-ga4-widget-layout"> 60 84 <div class="headless-ga4-widget-panel"> 61 <span class="headless-ga4-widget-label"><?php e sc_html_e('Sessions trend', 'headless-ga4'); ?></span>85 <span class="headless-ga4-widget-label"><?php echo esc_html($trend_label); ?></span> 62 86 <?php if (!empty($trend)) : ?> 63 87 <div class="headless-ga4-widget-chart"> 64 <?php echo headless_ga4_render_trend_svg($trend); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped?>88 <?php echo wp_kses(headless_ga4_render_trend_svg($trend, $trend_label), headless_ga4_get_allowed_chart_svg_html()); ?> 65 89 </div> 66 90 <div class="headless-ga4-widget-list"> … … 86 110 <strong class="headless-ga4-widget-item-title"><?php echo esc_html($headless_ga4_page['title'] !== '' ? (string) $headless_ga4_page['title'] : (string) $headless_ga4_page['path']); ?></strong> 87 111 <div class="headless-ga4-widget-muted"><?php echo esc_html((string) $headless_ga4_page['path']); ?></div> 112 <?php 113 echo wp_kses( 114 headless_ga4_render_horizontal_bar_svg( 115 (int) $headless_ga4_page['views'], 116 $headless_ga4_top_pages_max_views, 117 sprintf( 118 /* translators: %s: number of page views. */ 119 __('%s page views', 'headless-ga4'), 120 number_format_i18n((int) $headless_ga4_page['views']) 121 ) 122 ), 123 headless_ga4_get_allowed_chart_svg_html() 124 ); 125 ?> 88 126 </div> 89 127 <span class="headless-ga4-widget-muted"><?php echo esc_html(number_format_i18n((int) $headless_ga4_page['views'])); ?></span> … … 106 144 <strong class="headless-ga4-widget-item-title"><?php echo esc_html((string) $headless_ga4_source['source']); ?></strong> 107 145 <div class="headless-ga4-widget-muted"><?php echo esc_html((string) $headless_ga4_source['medium']); ?></div> 146 <?php 147 echo wp_kses( 148 headless_ga4_render_horizontal_bar_svg( 149 (int) $headless_ga4_source['sessions'], 150 $headless_ga4_top_sources_max_sessions, 151 sprintf( 152 /* translators: %s: number of sessions. */ 153 __('%s sessions', 'headless-ga4'), 154 number_format_i18n((int) $headless_ga4_source['sessions']) 155 ) 156 ), 157 headless_ga4_get_allowed_chart_svg_html() 158 ); 159 ?> 108 160 </div> 109 161 <span class="headless-ga4-widget-muted"><?php echo esc_html(number_format_i18n((int) $headless_ga4_source['sessions'])); ?></span> … … 135 187 <?php wp_nonce_field('headless_ga4_refresh'); ?> 136 188 <input type="hidden" name="action" value="headless_ga4_refresh" /> 189 <input type="hidden" name="headless_ga4_period" value="<?php echo esc_attr($current_period); ?>" /> 137 190 <?php submit_button(__('Refresh data', 'headless-ga4'), 'secondary', 'submit', false); ?> 138 191 </form> -
headless-ga4/trunk/assets/admin.css
r3504948 r3798238 73 73 } 74 74 75 .headless-ga4-widget-period-header { 76 display: flex; 77 align-items: center; 78 justify-content: space-between; 79 gap: 14px; 80 margin-bottom: 16px; 81 } 82 83 .headless-ga4-widget-period-header strong { 84 display: block; 85 margin-top: 4px; 86 font-size: 18px; 87 } 88 89 .headless-ga4-widget-period-filter { 90 display: flex; 91 flex-wrap: wrap; 92 gap: 6px; 93 } 94 95 .headless-ga4-widget-period-link { 96 border: 1px solid #c3c4c7; 97 border-radius: 999px; 98 background: #f6f7f7; 99 color: #1d2327; 100 padding: 5px 10px; 101 text-decoration: none; 102 } 103 104 .headless-ga4-widget-period-link:hover, 105 .headless-ga4-widget-period-link:focus { 106 border-color: #2271b1; 107 color: #0a4b78; 108 } 109 110 .headless-ga4-widget-period-link.is-active { 111 border-color: #1d2327; 112 background: #1d2327; 113 color: #fff; 114 } 115 75 116 .headless-ga4-widget-card, 76 117 .headless-ga4-widget-panel { … … 109 150 .headless-ga4-widget-list { 110 151 margin: 10px 0 0; 152 max-height: 230px; 153 overflow: auto; 111 154 } 112 155 … … 128 171 margin: 0; 129 172 font-size: 14px; 173 } 174 175 .headless-ga4-widget-rank-bar { 176 display: block; 177 width: min(180px, 100%); 178 height: 10px; 179 margin-top: 8px; 180 } 181 182 .headless-ga4-widget-rank-bar-track { 183 fill: #edf0f2; 184 } 185 186 .headless-ga4-widget-rank-bar-value { 187 fill: #2271b1; 130 188 } 131 189 … … 145 203 .headless-ga4-widget-chart { 146 204 margin-top: 10px; 205 border: 1px solid #ececec; 206 background: linear-gradient(180deg, #f8fbff 0%, #ffffff 100%); 207 padding: 10px; 147 208 } 148 209 … … 151 212 width: 100%; 152 213 height: auto; 214 } 215 216 .headless-ga4-widget-chart-bars rect { 217 fill: #d7e8f7; 218 } 219 220 .headless-ga4-widget-chart-line { 221 stroke: #111111; 222 stroke-linecap: round; 223 stroke-linejoin: round; 224 stroke-width: 3; 153 225 } 154 226 … … 170 242 171 243 @media (max-width: 782px) { 244 .headless-ga4-widget-period-header { 245 align-items: flex-start; 246 flex-direction: column; 247 } 248 172 249 .headless-ga4-widget-grid, 173 250 .headless-ga4-widget-layout { -
headless-ga4/trunk/headless-ga4.php
r3504948 r3798238 4 4 * Plugin URI: https://studio.jakubkanna.com/ 5 5 * Description: Display lightweight GA4 data in the WordPress dashboard for headless frontends. 6 * Version: 0. 1.16 * Version: 0.2.1 7 7 * Requires at least: 6.4 8 8 * Requires PHP: 7.4 … … 19 19 } 20 20 21 define('HEADLESS_GA4_VERSION', '0. 1.1');21 define('HEADLESS_GA4_VERSION', '0.2.1'); 22 22 define('HEADLESS_GA4_PLUGIN_FILE', __FILE__); 23 23 define('HEADLESS_GA4_PLUGIN_PATH', plugin_dir_path(__FILE__)); -
headless-ga4/trunk/includes/cache.php
r3504948 r3798238 55 55 56 56 /** 57 * Returns the transient name for a period-specific report cache. 58 * 59 * @param string $period Dashboard period key. 60 * 61 * @return string 62 */ 63 function headless_ga4_get_report_transient_name($period = 'weekly') 64 { 65 $period = is_string($period) ? sanitize_key($period) : 'weekly'; 66 67 if (!in_array($period, array('weekly', 'monthly', 'yearly', 'all_time'), true)) { 68 $period = 'weekly'; 69 } 70 71 return HEADLESS_GA4_REPORT_TRANSIENT . '_' . $period; 72 } 73 74 /** 57 75 * Returns the cached report payload. 76 * 77 * @param string $period Dashboard period key. 58 78 * 59 79 * @return array<string, mixed>|null 60 80 */ 61 function headless_ga4_get_cached_report( )81 function headless_ga4_get_cached_report($period = 'weekly') 62 82 { 63 $cached = get_transient( HEADLESS_GA4_REPORT_TRANSIENT);83 $cached = get_transient(headless_ga4_get_report_transient_name($period)); 64 84 65 85 return is_array($cached) ? $cached : null; … … 70 90 * 71 91 * @param array<string, mixed> $report Report payload. 92 * @param string $period Dashboard period key. 72 93 * 73 94 * @return void 74 95 */ 75 function headless_ga4_set_cached_report($report )96 function headless_ga4_set_cached_report($report, $period = 'weekly') 76 97 { 77 98 set_transient( 78 HEADLESS_GA4_REPORT_TRANSIENT,99 headless_ga4_get_report_transient_name($period), 79 100 $report, 80 101 headless_ga4_get_cache_ttl_seconds() … … 89 110 function headless_ga4_clear_cached_data() 90 111 { 112 $periods = array('weekly', 'monthly', 'yearly', 'all_time'); 113 91 114 delete_transient(HEADLESS_GA4_ACCESS_TOKEN_TRANSIENT); 92 115 delete_transient(HEADLESS_GA4_REPORT_TRANSIENT); 116 117 foreach ($periods as $period) { 118 delete_transient(headless_ga4_get_report_transient_name($period)); 119 } 93 120 } 94 -
headless-ga4/trunk/includes/dashboard-widget.php
r3504948 r3798238 43 43 44 44 /** 45 * Builds a simple SVG polyline chart for the sessions trend. 46 * 47 * @param array<int, array<string, mixed>> $trend Trend rows. 48 * 49 * @return string 50 */ 51 function headless_ga4_render_trend_svg($trend) 45 * Returns the dashboard period requested in the URL. 46 * 47 * @return string 48 */ 49 function headless_ga4_get_requested_dashboard_period() 50 { 51 $period = filter_input(INPUT_GET, 'headless_ga4_period', FILTER_SANITIZE_SPECIAL_CHARS); 52 53 return headless_ga4_normalize_dashboard_period(is_string($period) ? $period : 'weekly'); 54 } 55 56 /** 57 * Returns the dashboard period posted by the refresh form. 58 * 59 * @return string 60 */ 61 function headless_ga4_get_posted_dashboard_period() 62 { 63 $period = filter_input(INPUT_POST, 'headless_ga4_period', FILTER_SANITIZE_SPECIAL_CHARS); 64 65 return headless_ga4_normalize_dashboard_period(is_string($period) ? $period : 'weekly'); 66 } 67 68 /** 69 * Returns allowed SVG markup for dashboard charts. 70 * 71 * @return array<string, array<string, bool>> 72 */ 73 function headless_ga4_get_allowed_chart_svg_html() 74 { 75 return array( 76 'svg' => array( 77 'aria-label' => true, 78 'class' => true, 79 'role' => true, 80 'viewbox' => true, 81 'viewBox' => true, 82 ), 83 'g' => array( 84 'class' => true, 85 ), 86 'polyline' => array( 87 'class' => true, 88 'fill' => true, 89 'points' => true, 90 ), 91 'rect' => array( 92 'class' => true, 93 'height' => true, 94 'rx' => true, 95 'width' => true, 96 'x' => true, 97 'y' => true, 98 ), 99 ); 100 } 101 102 /** 103 * Builds an SVG bar and line diagram for the sessions trend. 104 * 105 * @param array<int, array<string, mixed>> $trend Trend rows. 106 * @param string $chart_label Accessible chart label. 107 * 108 * @return string 109 */ 110 function headless_ga4_render_trend_svg($trend, $chart_label = '') 52 111 { 53 112 if (empty($trend)) { … … 55 114 } 56 115 57 $width = 320; 58 $height = 92; 59 $padding = 8; 116 $width = 520; 117 $height = 180; 118 $padding_x = 18; 119 $padding_y = 16; 60 120 $values = array_map( 61 121 static function ($point) { … … 65 125 ); 66 126 $max_value = max(1, max($values)); 67 $min_value = min($values); 68 $range = max(1, $max_value - $min_value); 127 $range = max(1, $max_value); 69 128 $count = count($trend); 70 $step_x = $count > 1 ? ($width - ($padding * 2)) / ($count - 1) : 0; 129 $inner_width = $width - ($padding_x * 2); 130 $inner_height = $height - ($padding_y * 2); 131 $step_x = $count > 1 ? $inner_width / ($count - 1) : 0; 132 $bar_slot = $count > 0 ? $inner_width / $count : $inner_width; 133 $bar_width = max(3, min(22, $bar_slot * 0.55)); 71 134 $points = array(); 135 $bars = array(); 136 $chart_label = $chart_label !== '' ? $chart_label : __('Sessions trend', 'headless-ga4'); 72 137 73 138 foreach ($trend as $index => $point) { 74 139 $value = isset($point['value']) ? (int) $point['value'] : 0; 75 $x = $padding + ($step_x * $index); 76 $y = $height - $padding - ((($value - $min_value) / $range) * ($height - ($padding * 2))); 140 $x = $padding_x + ($step_x * $index); 141 $bar_height = max(1, ($value / $range) * $inner_height); 142 $bar_x = $x - ($bar_width / 2); 143 $bar_y = $height - $padding_y - $bar_height; 144 $y = $height - $padding_y - (($value / $range) * $inner_height); 77 145 $points[] = round($x, 2) . ',' . round($y, 2); 78 } 79 80 return '<svg viewBox="0 0 ' . esc_attr((string) $width) . ' ' . esc_attr((string) $height) . '" role="img" aria-label="' . esc_attr__('Sessions trend for the last 7 days', 'headless-ga4') . '"><polyline fill="none" stroke="#111111" stroke-width="2" points="' . esc_attr(implode(' ', $points)) . '" /></svg>'; 146 $bars[] = '<rect x="' . esc_attr((string) round($bar_x, 2)) . '" y="' . esc_attr((string) round($bar_y, 2)) . '" width="' . esc_attr((string) round($bar_width, 2)) . '" height="' . esc_attr((string) round($bar_height, 2)) . '" rx="3" />'; 147 } 148 149 return '<svg viewBox="0 0 ' . esc_attr((string) $width) . ' ' . esc_attr((string) $height) . '" role="img" aria-label="' . esc_attr($chart_label) . '"><g class="headless-ga4-widget-chart-bars">' . implode('', $bars) . '</g><polyline class="headless-ga4-widget-chart-line" fill="none" points="' . esc_attr(implode(' ', $points)) . '" /></svg>'; 150 } 151 152 /** 153 * Builds a compact horizontal bar SVG for ranked metric rows. 154 * 155 * @param int $value Metric value. 156 * @param int $max_value Largest value in the list. 157 * @param string $label Accessible chart label. 158 * 159 * @return string 160 */ 161 function headless_ga4_render_horizontal_bar_svg($value, $max_value, $label) 162 { 163 $value = max(0, (int) $value); 164 $max_value = max(1, (int) $max_value); 165 $width = 180; 166 $height = 10; 167 $bar_width = max(1, min($width, ($value / $max_value) * $width)); 168 169 return '<svg class="headless-ga4-widget-rank-bar" viewBox="0 0 ' . esc_attr((string) $width) . ' ' . esc_attr((string) $height) . '" role="img" aria-label="' . esc_attr($label) . '"><rect class="headless-ga4-widget-rank-bar-track" x="0" y="0" width="' . esc_attr((string) $width) . '" height="' . esc_attr((string) $height) . '" rx="5" /><rect class="headless-ga4-widget-rank-bar-value" x="0" y="0" width="' . esc_attr((string) round($bar_width, 2)) . '" height="' . esc_attr((string) $height) . '" rx="5" /></svg>'; 81 170 } 82 171 … … 93 182 94 183 check_admin_referer('headless_ga4_refresh'); 184 $period = headless_ga4_get_posted_dashboard_period(); 95 185 headless_ga4_clear_cached_data(); 96 $report = headless_ga4_fetch_dashboard_report(true );186 $report = headless_ga4_fetch_dashboard_report(true, $period); 97 187 98 188 if (is_wp_error($report)) { … … 102 192 } 103 193 104 wp_safe_redirect(ad min_url('index.php'));194 wp_safe_redirect(add_query_arg('headless_ga4_period', $period, admin_url('index.php'))); 105 195 exit; 106 196 } … … 122 212 $top_sources = array(); 123 213 $last_refreshed_at = 0; 214 $current_period = headless_ga4_get_requested_dashboard_period(); 215 $period_choices = headless_ga4_get_dashboard_period_choices(); 216 $period_label = isset($period_choices[$current_period]) ? $period_choices[$current_period] : __('Weekly', 'headless-ga4'); 217 $trend_label = __('Daily sessions', 'headless-ga4'); 218 $headless_ga4_top_pages_max_views = 0; 219 $headless_ga4_top_sources_max_sessions = 0; 124 220 $cards = array( 125 221 'users' => __('Users', 'headless-ga4'), … … 135 231 } 136 232 137 $report = headless_ga4_fetch_dashboard_report( );233 $report = headless_ga4_fetch_dashboard_report(false, $current_period); 138 234 139 235 if (is_wp_error($report)) { … … 150 246 $top_sources = isset($report['top_sources']) && is_array($report['top_sources']) ? $report['top_sources'] : array(); 151 247 $last_refreshed_at = isset($report['last_refreshed_at']) ? (int) $report['last_refreshed_at'] : 0; 248 $trend_label = isset($report['trend_label']) ? (string) $report['trend_label'] : $trend_label; 249 $period_label = isset($report['period_label']) ? (string) $report['period_label'] : $period_label; 250 251 foreach ($top_pages as $headless_ga4_top_page) { 252 $headless_ga4_top_pages_max_views = max( 253 $headless_ga4_top_pages_max_views, 254 isset($headless_ga4_top_page['views']) ? (int) $headless_ga4_top_page['views'] : 0 255 ); 256 } 257 258 foreach ($top_sources as $headless_ga4_top_source) { 259 $headless_ga4_top_sources_max_sessions = max( 260 $headless_ga4_top_sources_max_sessions, 261 isset($headless_ga4_top_source['sessions']) ? (int) $headless_ga4_top_source['sessions'] : 0 262 ); 263 } 152 264 153 265 require HEADLESS_GA4_PLUGIN_PATH . 'templates/dashboard-widget.php'; -
headless-ga4/trunk/includes/ga4-api.php
r3504948 r3798238 118 118 119 119 /** 120 * Returns the available dashboard reporting periods. 121 * 122 * @return array<string, string> 123 */ 124 function headless_ga4_get_dashboard_period_choices() 125 { 126 return array( 127 'weekly' => __('Weekly', 'headless-ga4'), 128 'monthly' => __('Monthly', 'headless-ga4'), 129 'yearly' => __('Yearly', 'headless-ga4'), 130 'all_time' => __('All time', 'headless-ga4'), 131 ); 132 } 133 134 /** 135 * Normalizes the dashboard period key. 136 * 137 * @param string $period Raw period key. 138 * 139 * @return string 140 */ 141 function headless_ga4_normalize_dashboard_period($period) 142 { 143 $period = is_string($period) ? sanitize_key($period) : 'weekly'; 144 $choices = headless_ga4_get_dashboard_period_choices(); 145 146 return isset($choices[$period]) ? $period : 'weekly'; 147 } 148 149 /** 150 * Returns report configuration for a dashboard period. 151 * 152 * @param string $period Dashboard period key. 153 * 154 * @return array<string, mixed> 155 */ 156 function headless_ga4_get_dashboard_period_config($period) 157 { 158 $period = headless_ga4_normalize_dashboard_period($period); 159 $choices = headless_ga4_get_dashboard_period_choices(); 160 $configs = array( 161 'weekly' => array( 162 'current_range' => array('startDate' => '6daysAgo', 'endDate' => 'today'), 163 'previous_range' => array('startDate' => '13daysAgo', 'endDate' => '7daysAgo'), 164 'trend_dimension' => 'date', 165 'trend_label' => __('Daily sessions', 'headless-ga4'), 166 'trend_limit' => 7, 167 ), 168 'monthly' => array( 169 'current_range' => array('startDate' => '29daysAgo', 'endDate' => 'today'), 170 'previous_range' => array('startDate' => '59daysAgo', 'endDate' => '30daysAgo'), 171 'trend_dimension' => 'date', 172 'trend_label' => __('Daily sessions', 'headless-ga4'), 173 'trend_limit' => 30, 174 ), 175 'yearly' => array( 176 'current_range' => array('startDate' => '365daysAgo', 'endDate' => 'today'), 177 'previous_range' => array('startDate' => '731daysAgo', 'endDate' => '366daysAgo'), 178 'trend_dimension' => 'yearMonth', 179 'trend_label' => __('Monthly sessions', 'headless-ga4'), 180 'trend_limit' => 13, 181 ), 182 'all_time' => array( 183 'current_range' => array('startDate' => '2020-10-14', 'endDate' => 'today'), 184 'previous_range' => null, 185 'trend_dimension' => 'year', 186 'trend_label' => __('Yearly sessions', 'headless-ga4'), 187 'trend_limit' => 100, 188 ), 189 ); 190 191 $config = $configs[$period]; 192 $config['key'] = $period; 193 $config['label'] = $choices[$period]; 194 195 return $config; 196 } 197 198 /** 199 * Formats a trend dimension value for display. 200 * 201 * @param string $value Raw GA4 dimension value. 202 * @param string $dimension GA4 dimension name. 203 * 204 * @return string 205 */ 206 function headless_ga4_format_trend_label($value, $dimension) 207 { 208 if ($dimension === 'date') { 209 $date = DateTime::createFromFormat('Ymd', $value, wp_timezone()); 210 211 return $date instanceof DateTime ? wp_date('M j', $date->getTimestamp()) : $value; 212 } 213 214 if ($dimension === 'yearMonth') { 215 $date = DateTime::createFromFormat('Ym', $value, wp_timezone()); 216 217 return $date instanceof DateTime ? wp_date('M Y', $date->getTimestamp()) : $value; 218 } 219 220 return $value; 221 } 222 223 /** 224 * Returns an empty metric summary. 225 * 226 * @return array<string, int> 227 */ 228 function headless_ga4_get_empty_metric_summary() 229 { 230 return array( 231 'activeUsers' => 0, 232 'sessions' => 0, 233 'screenPageViews' => 0, 234 'engagedSessions' => 0, 235 ); 236 } 237 238 /** 239 * Builds normalized trend points and fills empty buckets for the selected period. 240 * 241 * @param array<string, mixed> $trend_report Trend report. 242 * @param array<string, mixed> $period_config Period configuration. 243 * 244 * @return array<int, array<string, int|string>> 245 */ 246 function headless_ga4_build_trend_points($trend_report, $period_config) 247 { 248 $trend_rows = isset($trend_report['rows']) && is_array($trend_report['rows']) ? $trend_report['rows'] : array(); 249 $trend_dimension = isset($period_config['trend_dimension']) ? (string) $period_config['trend_dimension'] : 'date'; 250 $period = isset($period_config['key']) ? (string) $period_config['key'] : 'weekly'; 251 $values = array(); 252 253 foreach ($trend_rows as $row) { 254 $dimension_value = isset($row['dimensionValues'][0]['value']) ? (string) $row['dimensionValues'][0]['value'] : ''; 255 256 if ($dimension_value === '') { 257 continue; 258 } 259 260 $values[$dimension_value] = isset($row['metricValues'][0]['value']) ? (int) $row['metricValues'][0]['value'] : 0; 261 } 262 263 if ($period === 'weekly' || $period === 'monthly') { 264 $days = $period === 'weekly' ? 7 : 30; 265 $date = new DateTime('-' . ($days - 1) . ' days', wp_timezone()); 266 $trend = array(); 267 268 for ($index = 0; $index < $days; $index++) { 269 $key = $date->format('Ymd'); 270 $trend[] = array( 271 'label' => wp_date('M j', $date->getTimestamp()), 272 'value' => isset($values[$key]) ? (int) $values[$key] : 0, 273 ); 274 $date->modify('+1 day'); 275 } 276 277 return $trend; 278 } 279 280 if ($period === 'yearly') { 281 $date = new DateTime('first day of this month', wp_timezone()); 282 $date->modify('-12 months'); 283 $trend = array(); 284 285 for ($index = 0; $index < 13; $index++) { 286 $key = $date->format('Ym'); 287 $trend[] = array( 288 'label' => wp_date('M Y', $date->getTimestamp()), 289 'value' => isset($values[$key]) ? (int) $values[$key] : 0, 290 ); 291 $date->modify('+1 month'); 292 } 293 294 return $trend; 295 } 296 297 if (empty($values)) { 298 return array( 299 array( 300 'label' => wp_date('Y'), 301 'value' => 0, 302 ), 303 ); 304 } 305 306 $trend = array(); 307 308 foreach ($values as $dimension_value => $session_value) { 309 $trend[] = array( 310 'label' => headless_ga4_format_trend_label((string) $dimension_value, $trend_dimension), 311 'value' => (int) $session_value, 312 ); 313 } 314 315 return $trend; 316 } 317 318 /** 120 319 * Builds the normalized widget payload from GA4 report responses. 121 320 * … … 125 324 * @param array<string, mixed> $pages_report Top pages report. 126 325 * @param array<string, mixed> $sources_report Top sources report. 326 * @param array<string, mixed> $period_config Period configuration. 127 327 * 128 328 * @return array<string, mixed> 129 329 */ 130 function headless_ga4_build_dashboard_payload($current_summary, $previous_summary, $trend_report, $pages_report, $sources_report )330 function headless_ga4_build_dashboard_payload($current_summary, $previous_summary, $trend_report, $pages_report, $sources_report, $period_config) 131 331 { 132 332 $summary = array( … … 168 368 ); 169 369 170 $trend = array(); 171 $trend_rows = isset($trend_report['rows']) && is_array($trend_report['rows']) ? $trend_report['rows'] : array(); 172 173 foreach ($trend_rows as $row) { 174 $date_value = isset($row['dimensionValues'][0]['value']) ? (string) $row['dimensionValues'][0]['value'] : ''; 175 $session_value = isset($row['metricValues'][0]['value']) ? (int) $row['metricValues'][0]['value'] : 0; 176 177 if ($date_value === '') { 178 continue; 179 } 180 181 $date = DateTime::createFromFormat('Ymd', $date_value, wp_timezone()); 182 183 $trend[] = array( 184 'label' => $date instanceof DateTime ? wp_date('M j', $date->getTimestamp()) : $date_value, 185 'value' => $session_value, 186 ); 187 } 370 $trend = headless_ga4_build_trend_points($trend_report, $period_config); 188 371 189 372 $top_pages = array(); … … 221 404 'comparison' => $comparison, 222 405 'trend' => $trend, 406 'period' => isset($period_config['key']) ? (string) $period_config['key'] : 'weekly', 407 'period_label' => isset($period_config['label']) ? (string) $period_config['label'] : __('Weekly', 'headless-ga4'), 408 'trend_label' => isset($period_config['trend_label']) ? (string) $period_config['trend_label'] : __('Daily sessions', 'headless-ga4'), 223 409 'top_pages' => $top_pages, 224 410 'top_sources' => $top_sources, … … 231 417 * 232 418 * @param bool $force_refresh Whether to bypass the report cache. 419 * @param string $period Dashboard period key. 233 420 * 234 421 * @return array<string, mixed>|WP_Error 235 422 */ 236 function headless_ga4_fetch_dashboard_report($force_refresh = false )423 function headless_ga4_fetch_dashboard_report($force_refresh = false, $period = 'weekly') 237 424 { 238 425 if (!headless_ga4_is_configured()) { … … 243 430 } 244 431 432 $period = headless_ga4_normalize_dashboard_period($period); 433 $period_config = headless_ga4_get_dashboard_period_config($period); 434 $current_range = isset($period_config['current_range']) && is_array($period_config['current_range']) ? $period_config['current_range'] : array('startDate' => '6daysAgo', 'endDate' => 'today'); 435 $previous_range = isset($period_config['previous_range']) && is_array($period_config['previous_range']) ? $period_config['previous_range'] : null; 436 $trend_dimension = isset($period_config['trend_dimension']) ? (string) $period_config['trend_dimension'] : 'date'; 437 $trend_limit = isset($period_config['trend_limit']) ? (int) $period_config['trend_limit'] : 30; 438 245 439 if (!$force_refresh) { 246 $cached_report = headless_ga4_get_cached_report( );440 $cached_report = headless_ga4_get_cached_report($period); 247 441 248 442 if (is_array($cached_report)) { … … 254 448 array( 255 449 'dateRanges' => array( 256 array( 257 'startDate' => '6daysAgo', 258 'endDate' => 'today', 259 ), 450 $current_range, 260 451 ), 261 452 'metrics' => array( … … 272 463 } 273 464 274 $summary_previous = headless_ga4_run_report( 275 array( 276 'dateRanges' => array( 277 array( 278 'startDate' => '13daysAgo', 279 'endDate' => '7daysAgo', 465 if (is_array($previous_range)) { 466 $summary_previous = headless_ga4_run_report( 467 array( 468 'dateRanges' => array( 469 $previous_range, 280 470 ), 281 ), 282 'metrics' => array( 283 array('name' => 'activeUsers'), 284 array('name' => 'sessions'), 285 array('name' => 'screenPageViews'), 286 array('name' => 'engagedSessions'), 287 ), 288 ) 289 ); 290 291 if (is_wp_error($summary_previous)) { 292 return $summary_previous; 471 'metrics' => array( 472 array('name' => 'activeUsers'), 473 array('name' => 'sessions'), 474 array('name' => 'screenPageViews'), 475 array('name' => 'engagedSessions'), 476 ), 477 ) 478 ); 479 480 if (is_wp_error($summary_previous)) { 481 return $summary_previous; 482 } 483 484 $previous_summary = headless_ga4_map_metric_row($summary_previous); 485 } else { 486 $previous_summary = headless_ga4_get_empty_metric_summary(); 293 487 } 294 488 … … 296 490 array( 297 491 'dateRanges' => array( 298 array( 299 'startDate' => '6daysAgo', 300 'endDate' => 'today', 301 ), 492 $current_range, 302 493 ), 303 494 'dimensions' => array( 304 array('name' => 'date'),495 array('name' => $trend_dimension), 305 496 ), 306 497 'metrics' => array( … … 310 501 array( 311 502 'dimension' => array( 312 'dimensionName' => 'date',503 'dimensionName' => $trend_dimension, 313 504 ), 314 505 ), 315 506 ), 507 'limit' => $trend_limit, 316 508 ) 317 509 ); … … 324 516 array( 325 517 'dateRanges' => array( 326 array( 327 'startDate' => '6daysAgo', 328 'endDate' => 'today', 329 ), 518 $current_range, 330 519 ), 331 520 'dimensions' => array( … … 355 544 array( 356 545 'dateRanges' => array( 357 array( 358 'startDate' => '6daysAgo', 359 'endDate' => 'today', 360 ), 546 $current_range, 361 547 ), 362 548 'dimensions' => array( … … 385 571 $report = headless_ga4_build_dashboard_payload( 386 572 headless_ga4_map_metric_row($summary_current), 387 headless_ga4_map_metric_row($summary_previous),573 $previous_summary, 388 574 $trend_report, 389 575 $pages_report, 390 $sources_report 391 ); 392 393 headless_ga4_set_cached_report($report); 576 $sources_report, 577 $period_config 578 ); 579 580 headless_ga4_set_cached_report($report, $period); 394 581 395 582 return $report; 396 583 } 397 -
headless-ga4/trunk/includes/settings-page.php
r3504948 r3798238 116 116 117 117 if ($value === '') { 118 headless_ga4_set_option_autoload_no(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION);119 118 return $current; 120 119 } … … 134 133 135 134 headless_ga4_clear_cached_data(); 136 headless_ga4_set_option_autoload_no(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION);137 135 138 136 return (string) wp_json_encode( … … 304 302 305 303 check_admin_referer('headless_ga4_clear_credentials'); 306 update_option(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION, '');307 headless_ga4_set_option_autoload_no(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION);304 delete_option(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION); 305 add_option(HEADLESS_GA4_SERVICE_ACCOUNT_OPTION, '', '', false); 308 306 headless_ga4_clear_cached_data(); 309 307 headless_ga4_set_admin_notice(__('Stored service account credentials were cleared.', 'headless-ga4')); -
headless-ga4/trunk/readme.txt
r3504948 r3798238 5 5 Tested up to: 6.9 6 6 Requires PHP: 7.4 7 Stable tag: 0. 1.17 Stable tag: 0.2.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 121 121 == Changelog == 122 122 123 = 0.2.1 = 124 125 * Fix recursive service account JSON sanitization when saving settings 126 * Fix clearing stored service account credentials 127 128 = 0.2.0 = 129 130 * Add dashboard period filters for weekly, monthly, yearly, and all-time reports 131 * Add period-aware GA4 report caching 132 * Improve the sessions trend diagram with combined bar and line chart display 133 123 134 = 0.1.1 = 124 135 … … 137 148 == Upgrade Notice == 138 149 150 = 0.2.1 = 151 152 Fixes a critical error when saving service account JSON credentials. 153 154 = 0.2.0 = 155 156 Adds dashboard report period filters and an improved sessions diagram. 157 139 158 = 0.1.1 = 140 159 -
headless-ga4/trunk/templates/dashboard-widget.php
r3504948 r3798238 5 5 * @var array<string, string> $cards 6 6 * @var array<string, mixed> $comparison 7 * @var string $current_period 7 8 * @var int $last_refreshed_at 9 * @var string $period_label 10 * @var array<string, string> $period_choices 11 * @var int $headless_ga4_top_pages_max_views 12 * @var int $headless_ga4_top_sources_max_sessions 8 13 * @var string $state 9 14 * @var array<int, array<string, mixed>> $top_pages … … 11 16 * @var array<string, mixed> $summary 12 17 * @var array<int, array<string, mixed>> $trend 18 * @var string $trend_label 13 19 * @var string $error_message 14 20 */ … … 42 48 endif; 43 49 ?> 50 <div class="headless-ga4-widget-period-header"> 51 <div> 52 <span class="headless-ga4-widget-label"><?php esc_html_e('Report period', 'headless-ga4'); ?></span> 53 <strong><?php echo esc_html($period_label); ?></strong> 54 </div> 55 <div class="headless-ga4-widget-period-filter" aria-label="<?php esc_attr_e('Choose report period', 'headless-ga4'); ?>"> 56 <?php foreach ($period_choices as $headless_ga4_period_key => $headless_ga4_period_choice_label) : ?> 57 <a 58 class="headless-ga4-widget-period-link<?php echo $current_period === $headless_ga4_period_key ? ' is-active' : ''; ?>" 59 href="<?php echo esc_url(add_query_arg('headless_ga4_period', $headless_ga4_period_key, admin_url('index.php'))); ?>" 60 <?php echo $current_period === $headless_ga4_period_key ? 'aria-current="true"' : ''; ?> 61 > 62 <?php echo esc_html($headless_ga4_period_choice_label); ?> 63 </a> 64 <?php endforeach; ?> 65 </div> 66 </div> 67 44 68 <div class="headless-ga4-widget-grid"> 45 69 <?php foreach ($cards as $headless_ga4_card_key => $headless_ga4_card_label) : ?> … … 59 83 <div class="headless-ga4-widget-layout"> 60 84 <div class="headless-ga4-widget-panel"> 61 <span class="headless-ga4-widget-label"><?php e sc_html_e('Sessions trend', 'headless-ga4'); ?></span>85 <span class="headless-ga4-widget-label"><?php echo esc_html($trend_label); ?></span> 62 86 <?php if (!empty($trend)) : ?> 63 87 <div class="headless-ga4-widget-chart"> 64 <?php echo headless_ga4_render_trend_svg($trend); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped?>88 <?php echo wp_kses(headless_ga4_render_trend_svg($trend, $trend_label), headless_ga4_get_allowed_chart_svg_html()); ?> 65 89 </div> 66 90 <div class="headless-ga4-widget-list"> … … 86 110 <strong class="headless-ga4-widget-item-title"><?php echo esc_html($headless_ga4_page['title'] !== '' ? (string) $headless_ga4_page['title'] : (string) $headless_ga4_page['path']); ?></strong> 87 111 <div class="headless-ga4-widget-muted"><?php echo esc_html((string) $headless_ga4_page['path']); ?></div> 112 <?php 113 echo wp_kses( 114 headless_ga4_render_horizontal_bar_svg( 115 (int) $headless_ga4_page['views'], 116 $headless_ga4_top_pages_max_views, 117 sprintf( 118 /* translators: %s: number of page views. */ 119 __('%s page views', 'headless-ga4'), 120 number_format_i18n((int) $headless_ga4_page['views']) 121 ) 122 ), 123 headless_ga4_get_allowed_chart_svg_html() 124 ); 125 ?> 88 126 </div> 89 127 <span class="headless-ga4-widget-muted"><?php echo esc_html(number_format_i18n((int) $headless_ga4_page['views'])); ?></span> … … 106 144 <strong class="headless-ga4-widget-item-title"><?php echo esc_html((string) $headless_ga4_source['source']); ?></strong> 107 145 <div class="headless-ga4-widget-muted"><?php echo esc_html((string) $headless_ga4_source['medium']); ?></div> 146 <?php 147 echo wp_kses( 148 headless_ga4_render_horizontal_bar_svg( 149 (int) $headless_ga4_source['sessions'], 150 $headless_ga4_top_sources_max_sessions, 151 sprintf( 152 /* translators: %s: number of sessions. */ 153 __('%s sessions', 'headless-ga4'), 154 number_format_i18n((int) $headless_ga4_source['sessions']) 155 ) 156 ), 157 headless_ga4_get_allowed_chart_svg_html() 158 ); 159 ?> 108 160 </div> 109 161 <span class="headless-ga4-widget-muted"><?php echo esc_html(number_format_i18n((int) $headless_ga4_source['sessions'])); ?></span> … … 135 187 <?php wp_nonce_field('headless_ga4_refresh'); ?> 136 188 <input type="hidden" name="action" value="headless_ga4_refresh" /> 189 <input type="hidden" name="headless_ga4_period" value="<?php echo esc_attr($current_period); ?>" /> 137 190 <?php submit_button(__('Refresh data', 'headless-ga4'), 'secondary', 'submit', false); ?> 138 191 </form>
Note:
See TracChangeset
for help on using the changeset viewer.