| @@ -750,17 +750,29 @@ | ||
| 750 | 750 | // Fallback to old client if new one fails init (shouldn't happen if they use same creds) |
| 751 | 751 | $search_performance = $this->search_console_client->get_search_performance($site_url, $date_range, ['query'], 1000); |
| 752 | 752 | } |
| 753 | 753 | |
| 754 | - // Position distribution over every query with an | |
| 755 | - // impression, not over the 1,000-row list above (#913). | |
| 756 | - $position_distribution = $this->search_analytics_client | |
| 757 | - ? $this->count_position_distribution($site_url, $start_date, $end_date, $search_performance['rows'] ?? []) | |
| 758 | - : self::bucket_positions( | |
| 759 | - $search_performance['rows'] ?? [], | |
| 760 | - count($search_performance['rows'] ?? []) < 1000 | |
| 761 | - ); | |
| 754 | + // Calculate position distribution | |
| 755 | + $position_distribution = [ | |
| 756 | + 'top_3' => 0, | |
| 757 | + '4_10' => 0, | |
| 758 | + '10_50' => 0, | |
| 759 | + '51_100' => 0 | |
| 760 | + ]; | |
| 762 | 761 | |
| 762 | + foreach ($search_performance['rows'] ?? [] as $row) { | |
| 763 | + $position = $row['position'] ?? 0; | |
| 764 | + if ($position <= 3) { | |
| 765 | + $position_distribution['top_3']++; | |
| 766 | + } elseif ($position <= 10) { | |
| 767 | + $position_distribution['4_10']++; | |
| 768 | + } elseif ($position <= 50) { | |
| 769 | + $position_distribution['10_50']++; | |
| 770 | + } elseif ($position <= 100) { | |
| 771 | + $position_distribution['51_100']++; | |
| 772 | + } | |
| 773 | + } | |
| 774 | + | |
| 763 | 775 | $dashboard_data['search_performance'] = array_merge($search_performance, [ |
| 764 | 776 | 'totals' => $totals, |
| 765 | 777 | 'position_distribution' => $position_distribution |
| 766 | 778 | ]); |
| @@ -799,132 +811,8 @@ | ||
| 799 | 811 | // long-lived GSC payload has been stored. |
| 800 | 812 | $dashboard_data['core_web_vitals'] = $this->get_dashboard_core_web_vitals(); |
| 801 | 813 | |
| 802 | 814 | return $dashboard_data; |
| 803 | - } | |
| 804 | - | |
| 805 | - /** | |
| 806 | - * Rows per page when counting the position distribution. The most the | |
| 807 | - * Search Analytics API returns in one request. | |
| 808 | - * | |
| 809 | - * @since 2.15.0 | |
| 810 | - */ | |
| 811 | - private const POSITION_PAGE_SIZE = 25000; | |
| 812 | - | |
| 813 | - /** | |
| 814 | - * Pages read before the count stops: 200,000 queries. A property past | |
| 815 | - * that is counted over its top 200,000 by clicks and flagged incomplete. | |
| 816 | - * | |
| 817 | - * @since 2.15.0 | |
| 818 | - */ | |
| 819 | - private const POSITION_MAX_PAGES = 8; | |
| 820 | - | |
| 821 | - /** | |
| 822 | - * Count the period's queries into position buckets across the whole | |
| 823 | - * property (#913). | |
| 824 | - * | |
| 825 | - * The dashboard's query list is capped at 1,000 rows ordered by clicks, | |
| 826 | - * so counting it told any larger site it had exactly 1,000 queries and | |
| 827 | - * dropped the long tail, which is where positions 51-100 live. When that | |
| 828 | - * list came back short it already holds every query and is counted as | |
| 829 | - * is, with no extra request. Otherwise the property is paged with | |
| 830 | - * `startRow` at POSITION_PAGE_SIZE rows until a short page, counting as | |
| 831 | - * rows arrive rather than keeping them. | |
| 832 | - * | |
| 833 | - * A page that fails (other than a 401, which is re-thrown so the token | |
| 834 | - * refresh runs) leaves the count at what was read so far, flagged | |
| 835 | - * `complete: false`, rather than failing the whole dashboard. | |
| 836 | - * | |
| 837 | - * @since 2.15.0 | |
| 838 | - * | |
| 839 | - * @param string $site_url Search Console property. | |
| 840 | - * @param string $start_date Window start (Y-m-d). | |
| 841 | - * @param string $end_date Window end (Y-m-d). | |
| 842 | - * @param array $first_rows The capped query list already fetched. | |
| 843 | - * @return array{top_3:int,4_10:int,10_50:int,51_100:int,over_100:int,complete:bool} | |
| 844 | - * @throws \Exception On a 401, so get_dashboard_data() can refresh the token. | |
| 845 | - */ | |
| 846 | - private function count_position_distribution(string $site_url, string $start_date, string $end_date, array $first_rows): array { | |
| 847 | - if (count($first_rows) < 1000) { | |
| 848 | - return self::bucket_positions($first_rows, true); | |
| 849 | - } | |
| 850 | - | |
| 851 | - $distribution = self::bucket_positions([], true); | |
| 852 | - $start_row = 0; | |
| 853 | - | |
| 854 | - for ($page = 0; $page < self::POSITION_MAX_PAGES; $page++) { | |
| 855 | - try { | |
| 856 | - $rows = $this->search_analytics_client->get_search_analytics_data( | |
| 857 | - $site_url, | |
| 858 | - $start_date, | |
| 859 | - $end_date, | |
| 860 | - ['query'], | |
| 861 | - self::POSITION_PAGE_SIZE, | |
| 862 | - $start_row | |
| 863 | - )['rows'] ?? []; | |
| 864 | - } catch (\Exception $e) { | |
| 865 | - if ($e->getCode() === 401) { | |
| 866 | - throw $e; | |
| 867 | - } | |
| 868 | - // Nothing read yet: the capped list is the best there is. | |
| 869 | - $partial = $start_row === 0 ? self::bucket_positions($first_rows, false) : $distribution; | |
| 870 | - $partial['complete'] = false; | |
| 871 | - return $partial; | |
| 872 | - } | |
| 873 | - | |
| 874 | - $page_counts = self::bucket_positions($rows, true); | |
| 875 | - foreach (['top_3', '4_10', '10_50', '51_100', 'over_100'] as $bucket) { | |
| 876 | - $distribution[$bucket] += $page_counts[$bucket]; | |
| 877 | - } | |
| 878 | - | |
| 879 | - if (count($rows) < self::POSITION_PAGE_SIZE) { | |
| 880 | - return $distribution; | |
| 881 | - } | |
| 882 | - $start_row += self::POSITION_PAGE_SIZE; | |
| 883 | - } | |
| 884 | - | |
| 885 | - $distribution['complete'] = false; | |
| 886 | - return $distribution; | |
| 887 | - } | |
| 888 | - | |
| 889 | - /** | |
| 890 | - * Bucket Search Console rows by average position. | |
| 891 | - * | |
| 892 | - * `10_50` is the historical key for positions 11-50. Rows past 100 are | |
| 893 | - * counted in `over_100`: they still had impressions. | |
| 894 | - * | |
| 895 | - * @since 2.15.0 | |
| 896 | - * | |
| 897 | - * @param array $rows Search Console rows. | |
| 898 | - * @param bool $complete Whether $rows is every query in the window. | |
| 899 | - * @return array{top_3:int,4_10:int,10_50:int,51_100:int,over_100:int,complete:bool} | |
| 900 | - */ | |
| 901 | - private static function bucket_positions(array $rows, bool $complete): array { | |
| 902 | - $distribution = [ | |
| 903 | - 'top_3' => 0, | |
| 904 | - '4_10' => 0, | |
| 905 | - '10_50' => 0, | |
| 906 | - '51_100' => 0, | |
| 907 | - 'over_100' => 0, | |
| 908 | - 'complete' => $complete, | |
| 909 | - ]; | |
| 910 | - | |
| 911 | - foreach ($rows as $row) { | |
| 912 | - $position = (float) ($row['position'] ?? 0); | |
| 913 | - if ($position <= 3) { | |
| 914 | - $distribution['top_3']++; | |
| 915 | - } elseif ($position <= 10) { | |
| 916 | - $distribution['4_10']++; | |
| 917 | - } elseif ($position <= 50) { | |
| 918 | - $distribution['10_50']++; | |
| 919 | - } elseif ($position <= 100) { | |
| 920 | - $distribution['51_100']++; | |
| 921 | - } else { | |
| 922 | - $distribution['over_100']++; | |
| 923 | - } | |
| 924 | - } | |
| 925 | - | |
| 926 | - return $distribution; | |
| 927 | 815 | } |
| 928 | 816 | |
| 929 | 817 | /** |
| 930 | 818 | * Get Core Web Vitals for the analytics dashboard, cached independently |