Sindbad~EG File Manager

Current Path : /home/infinitibizsol/www.erofmesquite.com/wp-content/plugins/ahrefs-seo/php7/
Upload File :
Current File : /home/infinitibizsol/www.erofmesquite.com/wp-content/plugins/ahrefs-seo/php7/class-analytics-ga.php

<?php
declare( strict_types=1 );

namespace ahrefs\AhrefsSeo;

use ahrefs\AhrefsSeo\Messages\Message;
use ahrefs\AhrefsSeo\Options\Advanced;
use ahrefs\AhrefsSeo_Vendor\Google\Service\AnalyticsData\BatchRunReportsRequest;
use ahrefs\AhrefsSeo_Vendor\Google\Service\AnalyticsData\FilterExpressionList;
use ahrefs\AhrefsSeo_Vendor\Google\Service\AnalyticsData\Row as Google_Service_AnalyticsData_Row;
use ahrefs\AhrefsSeo_Vendor\Google\Service\AnalyticsData\RunReportRequest;
use ahrefs\AhrefsSeo_Vendor\Google\Service\AnalyticsData\RunReportResponse;
use ahrefs\AhrefsSeo_Vendor\Google\Service\GoogleAnalyticsAdmin;
use ahrefs\AhrefsSeo_Vendor\Google_Http_Batch;
use ahrefs\AhrefsSeo_Vendor\Google_Service_Analytics;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsData;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsData_DateRange;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsData_Dimension;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsData_Filter;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsData_FilterExpression;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsData_InListFilter;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsData_Metric;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsData_StringFilter;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsReporting;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsReporting_DateRange;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsReporting_Dimension;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsReporting_DimensionFilter;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsReporting_DimensionFilterClause;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsReporting_GetReportsRequest;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsReporting_Metric;
use ahrefs\AhrefsSeo_Vendor\Google_Service_AnalyticsReporting_ReportRequest;
use ahrefs\AhrefsSeo_Vendor\Google_Service_Exception;
use ahrefs\AhrefsSeo_Vendor\GuzzleHttp\Exception\ConnectException as GuzzleConnectException;
use ahrefs\AhrefsSeo_Vendor\GuzzleHttp\Exception\RequestException as GuzzleRequestException;
use Error;
use Exception;

trait Analytics_Ga {

	/**
	 * GA account: find items with the current domain and do some queries here
	 * Set found account as selected.
	 *
	 * @return string|null
	 */
	public function find_recommended_ga_id() : ?string {
		if ( defined( 'AHREFS_SEO_NO_GA' ) && AHREFS_SEO_NO_GA ) {
			return 'AHREFS_SEO_NO_GA';
		}
		$this->reset_pause( true, false );
		$list = $this->load_accounts_list();
		// recommended results, with the same domain in websiteUrl.
		$recommended = [];
		$details     = [];

		foreach ( $list as $account ) {
			if ( ! empty( $account['values'] ) ) {
				foreach ( $account['values'] as $property_name => $item ) {
					if ( isset( $item['views'] ) && count( $item['views'] ) ) {
						foreach ( $item['views'] as $view ) {
							if ( $this->is_ga_account_correct( $view['website'] ) ) {
								$recommended[]             = $view['ua_id'];
								$details[ $view['ua_id'] ] = [
									'name'    => $view['view'],
									'website' => $view['website'],
								];
							}
						}
					}
					if ( isset( $item['streams'] ) && count( $item['streams'] ) ) {
						$ua_id    = $item['streams'][0]['ua_id'];
						$websites = implode(
							'|',
							array_map(
								function( $stream ) {
									return $stream['website'];
								},
								$item['streams']
							)
						);
						if ( $this->is_ga_account_correct( $websites ) ) {
							$recommended[]     = $ua_id;
							$details[ $ua_id ] = [
								'name'    => $property_name,
								'website' => $websites,
							];
						}
					}
				}
			}
		}
		if ( ! count( $recommended ) ) {
			return null;
		}
		$counts = $this->check_ga_using_top_traffic_pages( $recommended );
		if ( is_null( $counts ) ) {
			return null;
		}
		arsort( $counts );
		reset( $counts );
		$ua_id = key( $counts );
		// set this account.
		if ( isset( $details[ $ua_id ] ) ) {
			$value = $details[ $ua_id ];
			wp_cache_flush();
			$this->get_data_tokens()->tokens_load();
			$this->set_ua( "$ua_id", $value['name'], $value['website'], $this->get_data_tokens()->get_gsc_site() );
		}

		return (string) $ua_id;
	}

	/**
	 * Return array with ua accounts list
	 *
	 * @return array<array>
	 */
	public function load_accounts_list() : array {
		$result = [];
		try {
			// mix ga4 with ga.
			$ga4  = $this->load_accounts_list_ga4();
			$ga   = $this->load_accounts_list_ga();
			$data = array_merge( $ga, $ga4 );
			// sort results.
			usort(
				$data,
				function( $a, $b ) {
					// order by account name.
					$diff = strcasecmp( $a['account_name'], $b['account_name'] );
					if ( 0 !== $diff ) {
						return $diff;
					}

					// then order by name.
					return strcasecmp( $a['name'], $b['name'] );
				}
			);
			// split by account, profile.
			foreach ( $data as $item ) {
				$account      = $item['account'];
				$account_name = $item['account_name'];
				$ua_id        = $item['ua_id'];
				$name         = $item['name'];
				$website      = $item['website'];
				if ( ! isset( $result[ $account ] ) ) {
					$result[ $account ] = [
						'account' => $account,
						'label'   => $account_name,
						'values'  => [],
					];
				}
				if ( ! isset( $result[ $account ]['values'][ $name ] ) ) {
					$result[ $account ]['values'][ $name ] = [];
				}
				$new_item = [
					'ua_id'   => $ua_id,
					'website' => $website,
				];
				$type     = null;
				if ( isset( $item['view'] ) ) {
					$type             = 'views';
					$new_item['view'] = $item['view'];
				} elseif ( isset( $item['stream'] ) ) {
					$type               = 'streams';
					$new_item['stream'] = $item['stream'];
				}
				if ( ! is_null( $type ) ) {
					if ( ! isset( $result[ $account ]['values'][ $name ][ $type ] ) ) {
						$result[ $account ]['values'][ $name ][ $type ] = [];
					}
					$result[ $account ]['values'][ $name ][ $type ][] = $new_item;
				}
			}
		} catch ( Error $e ) {
			$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__, __( 'Google Analytics API: failed to get the list of accounts.', 'ahrefs-seo' ) );
			$this->set_message( $message );
		}

		return $result;
	}

	/**
	 * Return array with ua accounts list from Google Analytics Admin API
	 *
	 * @return array<array>
	 *
	 * @since 0.7.3
	 *
	 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing -- we handle exception.
	 */
	protected function load_accounts_list_ga4() : array {
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar,WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		if ( is_array( $this->accounts_ga4 ) ) { // cached results from last call.
			return $this->accounts_ga4;
		}
		if ( defined( 'AHREFS_SEO_NO_GA' ) && AHREFS_SEO_NO_GA ) {
			return [];
		}
		$result     = [];
		$accounts   = [];
		$properties = [];
		try {
			$client = $this->create_client();
			$admin  = new GoogleAnalyticsAdmin( $client );

			// accounts and properties list.
			$next_list = '';
			do {
				$params = [
					'pageSize' => self::QUERY_LIST_GA_ACCOUNTS_PAGE_SIZE,
				];
				if ( ! empty( $next_list ) ) {
					$params['pageToken'] = $next_list;
				}
				$account_summaries = $admin->accountSummaries->listAccountSummaries( $params );
				$_accounts         = $account_summaries->getAccountSummaries();
				if ( count( $_accounts ) ) {
					foreach ( $_accounts as $_account ) {
						$account_name              = $_account->getAccount();
						$accounts[ $account_name ] = $_account->getDisplayName();

						$_properties = $_account->getPropertySummaries();
						if ( count( $_properties ) ) {
							foreach ( $_properties as $_property ) {
								$properties[ $_property['property'] ] = [
									'account' => $account_name,
									'label'   => $_property['displayName'],
								];
							}
						}
					}
				}
				$next_list = $account_summaries->getNextPageToken();
			} while ( ! empty( $next_list ) );
			$this->accounts_ga4_raw = $accounts;

			// get web streams for each property: need website urls.
			$streams  = []; // index is property id, value is array with data url.
			$requests = []; // Pending requests, [ property_id => next page token ].
			try {
				$client->setUseBatch( true );
				// prepare all initial requests.
				foreach ( $properties as $_property_id => $_values ) {
					$requests[ $_property_id ] = '';
				}

				$error_set = false;
				while ( ! empty( $requests ) ) {
					$pieces = array_splice( $requests, 0, 5 ); // execute up to 5 requests at once.
					$batch  = $admin->createBatch();
					foreach ( $pieces as $_property_id => $next_page ) {
						$params = [
							'pageSize' => self::QUERY_LIST_GA_ACCOUNTS_PAGE_SIZE,
						];
						if ( ! empty( $next_page ) ) {
							$params['pageToken'] = $next_page;
						}
						$request = $admin->properties_dataStreams->listPropertiesDataStreams( "{$_property_id}", $params );
						$batch->add( $request, $_property_id );
					}

					$responses = [];
					try {
						$responses = $batch->execute();
						do_action_ref_array( 'ahrefs_seo_api_list_ga4', [ &$responses ] );
					} catch ( Exception $e ) { // catch all errors.
						$this->set_message( $this->extract_message( $e ), $e );
						$this->on_error_received( $e );
						throw $e;
					}

					foreach ( $responses as $_property_id => $streams_list ) {
						if ( $streams_list instanceof Exception ) {
							if ( ! $error_set ) {
								$this->set_message( __( 'Could not receive a list of Google accounts. Google Analytics API returned an error. Please try again later or contact Ahrefs support to get it resolved.', 'ahrefs-seo' ) );
								$this->set_message( $this->extract_message( $streams_list ), $streams_list );
								$this->on_error_received( $streams_list );
								$error_set = true;
							}
							continue;
						}
						$_property_id = str_replace( 'response-', '', $_property_id );
						$_streams     = $streams_list->getDataStreams();
						if ( is_array( $_streams ) && count( $_streams ) ) {
							foreach ( $_streams as $_stream ) {
								$web_data = $_stream->webStreamData;
								if ( $web_data ) {
									if ( ! isset( $streams[ "$_property_id" ] ) ) {
										$streams[ "$_property_id" ] = [];
									}
									$streams[ "$_property_id" ][] = [
										'uri'   => $web_data->defaultUri,
										'label' => $_stream->displayName,
									];
								}
							}
						}
						$next_list = $streams_list->getNextPageToken();
						if ( ! empty( $next_list ) ) {
							$requests[ "$_property_id" ] = $next_list;
						}
					}
				}
			} finally {
				$client->setUseBatch( false );
			}

			if ( ! empty( $accounts ) && ! empty( $properties ) ) {
				foreach ( $properties as $property_id => $value ) {
					$account_id     = (string) $value['account'];
					$account_number = explode( '/', $account_id, 2 )[1] ?? '';
					$property_label = $value['label'];
					$account_label  = $accounts[ $account_id ] ?? '---';
					if ( ! empty( $streams[ $property_id ] ) ) {
						foreach ( $streams[ $property_id ] as $stream ) {
							$uri          = $stream['uri'];
							$stream_label = $stream['label'];
							$result[]     = [
								'ua_id'        => $property_id,
								'account'      => $account_number,
								'account_name' => $account_label,
								'name'         => $property_label,
								'stream'       => $stream_label,
								'website'      => $uri,
							];
						}
					}
				}
			}
			$this->accounts_ga4 = $result;
		} catch ( Error $e ) {
			$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
			$this->set_message( $message );
		} catch ( Google_Service_Exception $e ) {
			Ahrefs_Seo::breadcrumbs( 'Events ' . (string) wp_json_encode( $this->get_analytics_client()->get_logged_events() ) );
			Ahrefs_Seo::notify( $e );
			$this->set_message( $this->extract_message( $e, __( 'Google Analytics Admin API: failed to get the list of accounts.', 'ahrefs-seo' ) ) );
		} catch ( Exception $e ) {
			$this->set_message( $this->extract_message( $e, __( 'Google Analytics Admin API: failed to get the list of accounts.', 'ahrefs-seo' ) ), $e );
		}

		return $result;
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar,WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
	}

	/**
	 * Return array with ua accounts list from Google Analytics Management API
	 *
	 * @return array<array>
	 * @since 0.7.3
	 */
	protected function load_accounts_list_ga() : array {
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar,WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		if ( is_array( $this->accounts_ga ) ) { // cached results from last call.
			return $this->accounts_ga;
		}
		if ( defined( 'AHREFS_SEO_NO_GA' ) && AHREFS_SEO_NO_GA ) {
			return [
				[
					'ua_id'        => 'AHREFS_SEO_NO_GA',
					'account'      => 'AHREFS_SEO_NO_GA',
					'account_name' => 'AHREFS_SEO_NO_GA',
					'name'         => 'AHREFS_SEO_NO_GA',
					'view'         => __( 'default', 'ahrefs-seo' ),
					'website'      => 'https://' . Ahrefs_Seo::get_current_domain(),
				],
			];
		}
		$result = [];
		// do this call earlier, maybe it is no sence to make another calls if no accounts.
		try {
			$client    = $this->create_client();
			$analytics = new Google_Service_Analytics( $client );

			$ua_list = $analytics->management_webproperties->listManagementWebproperties( '~all' );
			do_action_ref_array( 'ahrefs_seo_api_list_ga_webproperties', [ &$ua_list ] );
		} catch ( Error $e ) {
			Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );

			return [];
		} catch ( Exception $e ) {
			$this->handle_exception( $e, false, true, false ); // do not save message.
			$this->set_message( $this->extract_message( $e, __( 'Google Analytics Management API: failed to get the list of accounts.', 'ahrefs-seo' ) ) );

			return [];
		}

		if ( empty( $ua_list ) ) {
			return [];
		}
		$data = $ua_list->getItems();

		try {
			$accounts_list = $analytics->management_accounts->listManagementAccounts();
			do_action_ref_array( 'ahrefs_seo_api_list_ga_accounts', [ &$accounts_list ] );
		} catch ( Exception $e ) {
			$this->handle_exception( $e );
			$accounts_list = null;
		}

		$accounts = [];
		if ( ! empty( $accounts_list ) ) {
			foreach ( $accounts_list->getItems() as $account ) {
				$accounts[ $account->getId() ] = $account->getName();
			}
			$this->accounts_ga_raw = array_values( $accounts );
		}

		/*
		Workaround to extract defaultProfileId, which some of the older GA accounts lack
		*/
		try {
			$profiles_list = $analytics->management_profiles->listManagementProfiles( '~all', '~all' );
			do_action_ref_array( 'ahrefs_seo_api_list_ga_profiles', [ &$profiles_list ] );
		} catch ( Exception $e ) {
			$this->handle_exception( $e );
			$profiles_list = null;
		}

		$profiles_groups = [];
		if ( ! empty( $profiles_list ) ) {
			foreach ( $profiles_list->getItems() as $profile ) {
				$_web_property_id = $profile->getWebPropertyId();
				if ( ! isset( $profiles_groups[ $_web_property_id ] ) ) {
					$profiles_groups[ $_web_property_id ] = [];
				}
				$profiles_groups[ $_web_property_id ][] = [
					'id'      => $profile->getId(),
					'name'    => $profile->getName(),
					'website' => $profile->getWebsiteUrl(),
				];
			}
		}

		if ( ! empty( $data ) ) {
			/** @var \ahrefs\AhrefsSeo_Vendor\Google_Service_Analytics_Webproperty $item */
			foreach ( $data as $item ) {
				if ( isset( $profiles_groups[ $item->id ] ) ) {
					foreach ( $profiles_groups[ $item->id ] as $_profile ) {
						$result[] = [
							'ua_id'        => $_profile['id'],
							'account'      => $item->accountId,
							'account_name' => $accounts[ $item->accountId ] ?? '---',
							'name'         => $item->name,
							'view'         => $_profile['name'],
							'website'      => $_profile['website'],
						];
					}
				} else {
					// fill default choice.
					$result[] = [
						'ua_id'        => $item->defaultProfileId,
						'account'      => $item->accountId,
						'account_name' => $accounts[ $item->accountId ] ?? '---',
						'name'         => $item->name,
						/* Translators: part of "default view" */
						'view'         => __( 'default', 'ahrefs-seo' ),
						'website'      => $item->websiteUrl,
					];
				}
			}
		}
		$this->accounts_ga = $result;

		return $result;
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar,WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
	}

	/**
	 * Get visitors traffic by type for page
	 *
	 * @param array<int|string, string>|null $page_slugs Page url starting with '/'.
	 * @param string                         $start_date Start date.
	 * @param string                         $end_date End date.
	 * @param null|int                       $max_results Max results.
	 * @param null|string                    $ua_id UA id.
	 *
	 * @return array<int|string, array<string, mixed>>|null Array, 'slug' => [ traffic type => visitors number].
	 */
	public function get_visitors_by_page( ?array $page_slugs, string $start_date, string $end_date, ?int $max_results = null, ?string $ua_id = null ) : ?array {
		Ahrefs_Seo::breadcrumbs( __METHOD__ . (string) wp_json_encode( func_get_args() ) );
		// is Analytics enabled?
		if ( ! $this->is_analytics_enabled() ) {
			$this->set_message( __( 'Analytics disconnected.', 'ahrefs-seo' ) );
			$this->service_error = [ [ 'reason' => 'internal-no-token' ] ];

			return null;
		}
		if ( is_null( $ua_id ) && ! $this->is_ua_set() ) {
			$this->set_message( __( 'Please choose Analytics profile.', 'ahrefs-seo' ) );
			$this->service_error = [ [ 'reason' => 'internal-no-profile' ] ];

			return null;
		}
		if ( is_array( $page_slugs ) && ! count( $page_slugs ) ) {
			return [];
		}

		$revert = [];
		if ( is_array( $page_slugs ) ) {
			foreach ( $page_slugs as $key => $value ) {
				$new_url            = $this->url_for_ga( $value );
				$revert[ $new_url ] = $value;
				$page_slugs[ $key ] = $new_url;
			}
		}

		if ( defined( 'AHREFS_SEO_NO_GA' ) && AHREFS_SEO_NO_GA ) {
			if ( is_null( $page_slugs ) ) {
				return [];
			} else {
				$result = [];
				foreach ( $page_slugs as $page_slug ) {
					/**
					 * Modify traffic values.
					 *
					 * @param array<string, int> $traffic The traffic values.
					 * @param string $page_slug Page slug.
					 * @param string $start_date Start date.
					 * @param string $end_date End date.
					 */
					$result[ $page_slug ] = apply_filters(
						'ahrefs_seo_no_ga_visitors_by_page',
						[
							Ahrefs_Seo_Analytics::TRAFFIC_TYPE_TOTAL   => 10,
							Ahrefs_Seo_Analytics::TRAFFIC_TYPE_ORGANIC => 5,
						],
						$page_slug,
						$start_date,
						$end_date
					);
				}

				return $result;
			}
		}

		$result = ( 0 === strpos( is_null( $ua_id ) ? $this->get_data_tokens()->get_ua_id() : $ua_id, 'properties/' ) )
			? $this->get_visitors_by_page_ga4( $page_slugs, $start_date, $end_date, $ua_id )
			: $this->get_visitors_by_page_ga( $page_slugs, $start_date, $end_date, $max_results, $ua_id );

		// add total => 0 to each missing slug.
		if ( ! is_null( $result ) && is_array( $page_slugs ) ) {
			foreach ( $page_slugs as $_slug ) {
				if ( ! isset( $result[ $_slug ] ) ) {
					$result[ $_slug ] = [ Ahrefs_Seo_Analytics::TRAFFIC_TYPE_TOTAL => 0 ];
				}
			}
		}

		// set back to original URLs.
		$result2 = [];
		if ( ! is_null( $result ) ) {
			foreach ( $result as $slug => $value ) {
				if ( isset( $revert[ $slug ] ) ) { // sometimes returned value has chars in different case.
					$result2[ $revert[ $slug ] ] = $value;
				} else {
					$result2[ $slug ] = $value;
				}
			}
		}

		Ahrefs_Seo::breadcrumbs( 'get_visitors_by_page: ' . (string) wp_json_encode( $page_slugs ) . ' results: ' . (string) wp_json_encode( $result2 ) );

		return $result2;
	}

	/**
	 * Prepare URL for GA request.
	 *
	 * @param string $url Original URL.
	 *
	 * @return string
	 * @since 0.9.4
	 */
	private function url_for_ga( string $url ) : string {
		$advanced = new Advanced();
		if ( $advanced->get_adv_ga_uses_full_url() ) {
			$url = Ahrefs_Seo::get_current_domain() . $url;
		}

		return $advanced->get_adv_ga_not_urlencoded() ? urldecode( $url ) : $url;
	}

	/**
	 * Get visitors traffic by type for page for GA4 property, use Google Analytics Data API
	 *
	 * @param null|array<int|string, string> $page_slugs_list Page url starting with '/'.
	 * @param string                         $start_date Start date.
	 * @param string                         $end_date End date.
	 * @param null|string                    $ua_id UA id or null if default UA id used.
	 *
	 * @return array<int|string, array<string, mixed>> Array, 'slug' => [ traffic type => visitors number]
	 * @phpstan-return array<int|string, array<Ahrefs_Seo_Analytics::TRAFFIC_TYPE_ORGANIC|Ahrefs_Seo_Analytics::TRAFFIC_TYPE_TOTAL, int>|array<"error", string>>
	 * @since 0.7.3
	 *
	 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing -- we handle exception.
	 */
	protected function get_visitors_by_page_ga4( ?array $page_slugs_list, string $start_date, string $end_date, ?string $ua_id = null ) : ?array {
		$result = [];
		try {
			$client     = $this->create_client();
			$analytics4 = new Google_Service_AnalyticsData( $client );

			if ( is_null( $ua_id ) ) {
				$ua_id = $this->get_data_tokens()->get_ua_id();
			}
			// numeric part only.
			$property_id = str_replace( 'properties/', '', $ua_id );
			$page_slugs  = empty( $page_slugs_list ) ? null : $page_slugs_list; // receive pages info without slug filter.
			$per_page    = is_array( $page_slugs ) ? count( $page_slugs ) : self::QUERY_TRAFFIC_PER_PAGE;
			$offset      = 0;

			try {
				$data = null;
				// analytics additional parameters.
				$params = [
					'quotaUser' => $this->get_api_user(),
				];
				// get results from GA4.
				try {
					$this->maybe_do_a_pause( 'ga4' );
					$batch   = new BatchRunReportsRequest();
					$request = $this->create_ga4_request( $start_date, $end_date, $per_page, $offset );
					if ( ! is_null( $page_slugs ) ) { // request for specified urls list.
						$this->apply_ga4_filter( $request, $page_slugs );
					}
					if ( ! $this->set_ga4_property( $request, $property_id ) ) {
						/* Translators: 1: version string, 2: function name, 3: line number */
						throw new Ahrefs_Seo_Compatibility_Exception( sprintf( __( 'Unsupported Google Analytics Data API version %1$s at %2$s line %3$d', 'ahrefs-seo' ), $analytics4->version, __METHOD__, __LINE__ ) );
					}
					$request->setReturnPropertyQuota( true );

					$request2 = $this->create_ga4_request( $start_date, $end_date, $per_page, $offset );
					// request for organic traffic and specified urls list.
					$this->apply_ga4_filter( $request2, $page_slugs, true );
					if ( ! $this->set_ga4_property( $request2, $property_id ) ) {
						/* Translators: 1: version string, 2: function name, 3: line number */
						throw new Ahrefs_Seo_Compatibility_Exception( sprintf( __( 'Unsupported Google Analytics Data API version %1$s at %2$s line %3$d', 'ahrefs-seo' ), $analytics4->version, __METHOD__, __LINE__ ) );
					}
					$request2->setReturnPropertyQuota( true );

					$batch->setRequests( [ $request, $request2 ] );
					Ahrefs_Seo::breadcrumbs( sprintf( 'ga4-req prop:%s par:%s batch:%s', 'properties/' . $property_id, (string) wp_json_encode( $params ), (string) wp_json_encode( $batch ) ) );

					if ( property_exists( $analytics4, 'properties' ) && is_object( $analytics4->properties ) && method_exists( $analytics4->properties, 'batchRunReports' ) ) { // @phpstan-ignore-line -- currently unstable v1beta used, next version may not have this property.
						$reports = $analytics4->properties->batchRunReports( 'properties/' . $property_id, $batch, $params );
					} else {
						/* Translators: 1: version string, 2: function name, 3: line number */
						throw new Ahrefs_Seo_Compatibility_Exception( sprintf( __( 'Unsupported Google Analytics Data API version %1$s at %2$s line %3$d', 'ahrefs-seo' ), $analytics4->version, __METHOD__, __LINE__ ) );
					}
					do_action_ref_array( 'ahrefs_seo_api_visitors_by_page_batch_ga4', [ &$reports ] );
					$this->maybe_do_a_pause( 'ga4', true );
				} catch ( Google_Service_Exception $e ) { // catch recoverable errors.
					Ahrefs_Seo::breadcrumbs( sprintf( 'batch:%s resp:%s', (string) wp_json_encode( $batch ?? null ), (string) wp_json_encode( $reports ?? null ) ) );
					$this->maybe_do_a_pause( 'ga4', true );
					$this->service_error = $e->getErrors();
					$this->handle_exception( $e );
					$this->on_error_received( $e, $page_slugs_list );
					throw $e;
				} catch ( GuzzleConnectException $e ) { // catch recoverable errors.
					$this->maybe_do_a_pause( 'ga4', true );
					$this->set_message( $this->extract_message( $e ), $e, (string) wp_json_encode( $request ?? null ) );
					$this->on_error_received( $e, $page_slugs_list );
					throw $e;
				}

				if ( ! empty( $reports ) ) {
					$reports_data = $reports->getReports();

					if ( is_array( $reports_data ) && ( 2 === count( $reports_data ) ) && ( 2 === count(
						array_filter(
							$reports_data,
							function( $item ) {
									return is_object( $item ) && ( $item instanceof RunReportResponse );
							}
						)
					) ) ) {
						$report_total   = $reports_data[0];
						$report_organic = $reports_data[1];

						$rows = $report_total->getRows();
						if ( ! empty( $rows ) ) {
							$this->parse_ga4_rows( $result, $rows, Ahrefs_Seo_Analytics::TRAFFIC_TYPE_TOTAL );
						}
						$rows = $report_organic->getRows();
						if ( ! empty( $rows ) ) {
							$this->parse_ga4_rows( $result, $rows, Ahrefs_Seo_Analytics::TRAFFIC_TYPE_ORGANIC );
						}
					} else {
						Ahrefs_Seo::notify( new Ahrefs_Seo_Exception( 'Incorrect ga4 batch response: ' . (string) wp_json_encode( $reports ) ) );
					}
				}
			} catch ( Error $e ) {
				$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
				$this->set_message( $message );
			} catch ( Exception $e ) {
				$this->handle_exception( $e, true );

				return $this->prepare_answer( $page_slugs_list, __( 'Connection error', 'ahrefs-seo' ) );
			}
		} catch ( Error $e ) {
			$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
			$this->set_message( $message );
		}

		return $result;
	}

	/**
	 * @param array                              $result Destination array with results.
	 * @param Google_Service_AnalyticsData_Row[] $rows Results from GA API.
	 * @param string                             $type Traffic type.
	 *
	 * @return void
	 */
	protected function parse_ga4_rows( array &$result, array $rows, string $type = Ahrefs_Seo_Analytics::TRAFFIC_TYPE_TOTAL ) : void {
		foreach ( $rows as $row ) {
			$dimensions     = $row->getDimensionValues(); // page slug + traffic type.
			$_slug          = $dimensions[0]->getValue();
			$_metrics       = $row->getMetricValues();
			$_traffic_count = (int) ( $_metrics[0]->getValue() ?? 0 );

			if ( ! isset( $result[ $_slug ] ) ) {
				$result[ $_slug ] = [];
			}
			if ( ! isset( $result[ $_slug ][ "$type" ] ) ) {
				$result[ $_slug ][ "$type" ] = $_traffic_count;
			} else {
				$result[ $_slug ][ "$type" ] += $_traffic_count;
			}
		}
	}

	/**
	 * Creates filter for pages list.
	 *
	 * @param array $page_slugs Pages slug list.
	 *
	 * @return Google_Service_AnalyticsData_FilterExpression
	 */
	protected function create_filter_paths( array $page_slugs ) : Google_Service_AnalyticsData_FilterExpression {
		$in_list_filter = new Google_Service_AnalyticsData_InListFilter();
		$in_list_filter->setValues( $page_slugs );

		$filter = new Google_Service_AnalyticsData_Filter();
		$filter->setFieldName( 'pagePath' );
		$filter->setInListFilter( $in_list_filter );

		$dimension_filter = new Google_Service_AnalyticsData_FilterExpression();
		$dimension_filter->setFilter( $filter );

		return $dimension_filter;
	}

	/**
	 * Creates filter for "Organic Search" traffic.
	 *
	 * @return Google_Service_AnalyticsData_FilterExpression
	 */
	protected function create_filter_organic() : Google_Service_AnalyticsData_FilterExpression {
		$string_filter = new Google_Service_AnalyticsData_StringFilter();
		$string_filter->setValue( Ahrefs_Seo_Analytics::TRAFFIC_TYPE_ORGANIC );

		$filter = new Google_Service_AnalyticsData_Filter();
		$filter->setFieldName( 'sessionDefaultChannelGrouping' );
		$filter->setStringFilter( $string_filter );

		$dimension_filter = new Google_Service_AnalyticsData_FilterExpression();
		$dimension_filter->setFilter( $filter );

		return $dimension_filter;
	}

	/**
	 * Apply filters to the prepared request
	 *
	 * @param RunReportRequest $request Request.
	 * @param array|null       $page_slugs List of page url starting with '/'.
	 * @param bool             $only_organic_traffic Load data for Organic traffic type only.
	 *
	 * @return void
	 */
	protected function apply_ga4_filter( RunReportRequest &$request, ?array $page_slugs, bool $only_organic_traffic = false ) : void {
		if ( $only_organic_traffic ) {
			if ( ! is_null( $page_slugs ) ) { // filter by sessionDefaultChannelGrouping = "Organic Search" AND for specified urls list.
				$dimension_filter1 = $this->create_filter_paths( $page_slugs );
				$dimension_filter2 = $this->create_filter_organic();

				$filter_expression_list = new FilterExpressionList();
				$filter_expression_list->setExpressions( [ $dimension_filter1, $dimension_filter2 ] );

				$dimension_filter = new Google_Service_AnalyticsData_FilterExpression();
				$dimension_filter->setAndGroup( $filter_expression_list );

				$request->setDimensionFilter( $dimension_filter );
			} else { // filter only by sessionDefaultChannelGrouping = "Organic Search".
				$dimension_filter = $this->create_filter_organic();
				$request->setDimensionFilter( $dimension_filter );
			}
		} else {
			if ( ! is_null( $page_slugs ) ) { // filter for specified urls list.
				$dimension_filter = $this->create_filter_paths( $page_slugs );
				$request->setDimensionFilter( $dimension_filter );
			}
		}
	}

	/**
	 * Create the request
	 *
	 * @param string $start_date Start date.
	 * @param string $end_date End date.
	 * @param int    $per_page Number of results.
	 * @param int    $offset Results offset.
	 *
	 * @return RunReportRequest
	 */
	protected function create_ga4_request( string $start_date, string $end_date, int $per_page, int $offset ) : RunReportRequest {
		// Create the DateRange object.
		$date_range = new Google_Service_AnalyticsData_DateRange();
		$date_range->setStartDate( $start_date );
		$date_range->setEndDate( $end_date );

		// Create the Metrics object.
		/** @link https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema#metrics */
		$metric = new Google_Service_AnalyticsData_Metric();
		$metric->setName( 'screenPageViews' ); // "ga:uniquePageviews"

		// Create the Dimension object.
		/** @link https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema#dimensions */
		$dimension1 = new Google_Service_AnalyticsData_Dimension();
		$dimension1->setName( 'pagePath' ); // "ga:pagePath".

		// Create the ReportRequest object.
		$request = new RunReportRequest();
		$request->setDateRanges( $date_range );
		$request->setMetrics( array( $metric ) );
		$request->setDimensions( array( $dimension1 ) );
		$request->setLimit( $per_page );
		$request->setOffset( $offset );

		return $request;
	}

	/**
	 * Fill answers with error message
	 *
	 * @param int[]|string[]|null $page_slugs_list Page slugs list.
	 * @param string              $error_message Error message.
	 *
	 * @return array Index is slug, value is ['error' => $error_message].
	 * @since 0.7.3
	 */
	protected function prepare_answer( ?array $page_slugs_list, string $error_message ) : ?array {
		return is_null( $page_slugs_list ) ? null : array_map(
			function( $slug ) use ( $error_message ) {
				return [ 'error' => $error_message ];
			},
			array_flip( $page_slugs_list )
		);
	}

	/**
	 * Get visitors traffic by type for page for GA property, use Google Analytics Reporting API version 4.
	 *
	 * @param array<int|string, string>|null $page_slugs_list Page url starting with '/'.
	 * @param string                         $start_date Start date.
	 * @param string                         $end_date End date.
	 * @param null|int                       $max_results Max results count.
	 * @param null|string                    $ua_id UA id or null if default UA id used.
	 *
	 * @return array<int|string, array<string, mixed>> Array, 'slug' => [ traffic type => visitors number].
	 * @since 0.7.3
	 *
	 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing -- we handle exception.
	 */
	public function get_visitors_by_page_ga( ?array $page_slugs_list, string $start_date, string $end_date, ?int $max_results = null, ?string $ua_id = null ) : ?array {
		$result = [];
		try {
			$client             = $this->create_client();
			$analyticsreporting = new Google_Service_AnalyticsReporting( $client );
			if ( is_null( $ua_id ) ) {
				$ua_id = $this->get_data_tokens()->get_ua_id();
			}
			$page_slugs = is_null( $page_slugs_list ) ? [ null ] : $page_slugs_list; // receive pages info without slug filter.
			$per_page   = is_null( $max_results ) ? self::QUERY_TRAFFIC_PER_PAGE : $max_results;

			$pages_to_load = array_map(
				function( $slug ) {
					return [
						'slug'       => $slug,
						'next_token' => null,
					]; // later we will add next_token or remove item from the list.
				},
				$page_slugs
			);

			do {
				try {
					$requests = []; // up to 5 requests allowed.
					$data     = null;
					// analytics parameters.
					$params = [
						'quotaUser' => $this->get_api_user(),
					];

					// get results from Google Analytics.
					try {
						$this->maybe_do_a_pause( 'ga' );

						foreach ( $pages_to_load as $page_to_load ) {
							$page_slug  = $page_to_load['slug'];
							$next_token = $page_to_load['next_token'] ?? null;

							// Create the DateRange object.
							$request = $this->create_report_request_object( $start_date, $end_date, $ua_id, $per_page );

							if ( ! is_null( $page_slug ) ) {
								// Create the DimensionFilter.
								$dimension_filter = new Google_Service_AnalyticsReporting_DimensionFilter();
								$dimension_filter->setDimensionName( 'ga:pagePath' );
								$dimension_filter->setOperator( 'EXACT' );
								$dimension_filter->setExpressions( array( $page_slug ) );

								// Create the DimensionFilterClauses.
								$dimension_filter_clause = new Google_Service_AnalyticsReporting_DimensionFilterClause();
								$dimension_filter_clause->setFilters( array( $dimension_filter ) );
								$request->setDimensionFilterClauses( array( $dimension_filter_clause ) );
							}

							if ( ! empty( $next_token ) ) {
								$request->setPageToken( $next_token );
							}

							$requests[] = $request;
						}

						$body = new Google_Service_AnalyticsReporting_GetReportsRequest();
						$body->setReportRequests( $requests );
						$data = $analyticsreporting->reports->batchGet( $body, $params );
						do_action_ref_array( 'ahrefs_seo_api_visitors_by_page_ga', [ &$data ] );
						$this->maybe_do_a_pause( 'ga', true );
					} catch ( Google_Service_Exception $e ) { // catch recoverable errors.
						$this->maybe_do_a_pause( 'ga', true );
						$this->service_error = $e->getErrors();
						$this->handle_exception( $e );
						$this->on_error_received( $e, $page_slugs_list );
						throw $e;
					} catch ( GuzzleRequestException $e ) { // catch recoverable errors.
						$this->maybe_do_a_pause( 'ga', true );
						$this->handle_exception( $e );
						$this->on_error_received( $e, $page_slugs_list );
						throw $e;
					}

					if ( ! is_null( $data ) ) {
						$reports = $data->getReports();
						if ( ! empty( $reports ) ) {
							foreach ( $reports as $index => $report ) {
								$data_items                            = $report->getData();
								$pages_to_load[ $index ]['next_token'] = $report->getNextPageToken();

								// load details from rows.
								$rows = $data_items->getRows();
								if ( ! empty( $rows ) ) {
									foreach ( $rows as $row ) {
										list( $_slug, $_type ) = $row->getDimensions(); // page slug + traffic type.

										$_metrics       = $row->getMetrics();
										$_traffic_count = ( $_metrics[0]->getValues() )[0] ?? 0;

										if ( ! isset( $result[ $_slug ] ) ) {
											$result[ $_slug ] = [];
										}
										if ( ! isset( $result[ $_slug ][ "$_type" ] ) ) {
											$result[ $_slug ][ "$_type" ]                                 = (int) $_traffic_count;
											$result[ $_slug ][ Ahrefs_Seo_Analytics::TRAFFIC_TYPE_TOTAL ] = (int) $_traffic_count + ( $result[ $_slug ][ Ahrefs_Seo_Analytics::TRAFFIC_TYPE_TOTAL ] ?? 0 );
										} else {
											$result[ $_slug ][ "$_type" ]                                 += (int) $_traffic_count;
											$result[ $_slug ][ Ahrefs_Seo_Analytics::TRAFFIC_TYPE_TOTAL ] += (int) $_traffic_count;
										}
									}
								}
								if ( ! is_null( $max_results ) && ( count( $rows ) >= $max_results || count( $result ) >= $max_results ) ) {
									$pages_to_load[ $index ]['next_token'] = null; // do not load more.
								}
							}
						} else {
							$pages_to_load = [];
						}
					} else {
						$pages_to_load = [];
					}
					// remove finished pages (without next_token) from load list.
					$pages_to_load = array_values(
						array_filter(
							$pages_to_load,
							function( $value ) {
								return ! empty( $value['next_token'] );
							}
						)
					);
				} catch ( Error $e ) {
					$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
					$this->set_message( $message );
				} catch ( Exception $e ) {
					$this->handle_exception( $e, true );

					return $this->prepare_answer( $page_slugs_list, __( 'Connection error', 'ahrefs-seo' ) );
				}
				// load until any next page exists, but load only first page with results for the generic request without page ($page_slugs_list is null).
			} while ( ! empty( $pages_to_load ) && ! is_null( $page_slugs_list ) && ! is_null( $data ) );
		} catch ( Error $e ) {
			$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
			$this->set_message( $message );
		}

		return $result;
	}

	/**
	 * Get top pages for current GA profile.
	 *
	 * @return string[]|null
	 * @since 0.9.4
	 */
	public function get_top_ga_results() : ?array {
		if ( ! $this->is_analytics_enabled() ) {
			return null;
		}
		$start_date = (string) date( 'Y-m-d', time() - 3 * MONTH_IN_SECONDS );
		$end_date   = (string) date( 'Y-m-d' );
		$ua_id      = $this->get_data_tokens()->get_ua_id();
		if ( '' === $ua_id ) {
				return null;
		}
		if ( 0 === strpos( $ua_id, 'properties/' ) ) {
			$result = $this->get_found_pages_by_ua_id_ga4( [ $ua_id ], $start_date, $end_date, false );
		} else {
			$result = $this->get_found_pages_by_ua_id_ga( [ $ua_id ], $start_date, $end_date, false );
		}
		$item = array_shift( $result );
		return is_array( $item ) ? $item : null;
	}

	/**
	 * Get visitors traffic by type for page for GA property.
	 *
	 * @param string[] $ua_ids UA ids list to check.
	 * @param string   $start_date Start date.
	 * @param string   $end_date End date.
	 * @param bool     $return_count Return count of found pages or pages slugs list.
	 *
	 * @return array<string, null|int>|array<string, null|string[]> Array, [ ua_id => pages_found ].
	 * @phpstan-return ($return_count is true ? array<string, null|int> : array<string, null|string[]>)
	 * @since 0.7.3
	 */
	private function get_found_pages_by_ua_id_ga( array $ua_ids, string $start_date, string $end_date, bool $return_count = true ) : array {
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		$results = [];
		try {
			$client = $this->create_client();
			$client->setUseBatch( true );

			$analyticsreporting = new Google_Service_AnalyticsReporting( $client );

			$per_page = $return_count ? self::QUERY_DETECT_GA_LIMIT : 1000; // used as per page parameter, but really we load first page only.
			do { // for ua_ids parts.
				$ua_id_list = array_splice( $ua_ids, 0, 5 ); // max 5 requests per batch.
				try {
					$data = null;
					// analytics parameters.
					$params = [
						'quotaUser' => $this->get_api_user(),
					];

					// get results from Google Analytics.
					try {
						$this->maybe_do_a_pause( 'ga' );
						$batch = new Google_Http_Batch(
							$client,
							false,
							$analyticsreporting->rootUrl,
							$analyticsreporting->batchPath
						);
						$this->maybe_do_a_pause( 'ga', true );

						foreach ( $ua_id_list as $ua_id ) {
							// Create the DateRange object.
							$request = $this->create_report_request_object( $start_date, $end_date, $ua_id, $per_page );

							$body = new Google_Service_AnalyticsReporting_GetReportsRequest();
							$body->setReportRequests( [ $request ] );
							$prepared_queries = $analyticsreporting->reports->batchGet( $body, $params );
							$batch->add( $prepared_queries, $ua_id );
						}
						$data = $batch->execute();
					} catch ( Google_Service_Exception $e ) { // try to continue, but report error.
						Ahrefs_Seo_Errors::save_message( 'google', $e->getMessage(), Message::TYPE_NOTICE );
						Ahrefs_Seo::notify( $e, 'autodetect ga' );
					} catch ( GuzzleConnectException $e ) { // try to continue, but report error.
						Ahrefs_Seo_Errors::save_message( 'google', $e->getMessage(), Message::TYPE_NOTICE );
						Ahrefs_Seo::notify( $e, 'autodetect ga' );
					}
					if ( ! is_null( $data ) ) {
						foreach ( $data as $index => $values ) {
							$result      = [];
							$result_list = [];
							$index       = str_replace( 'response-', '', $index );
							if ( $values instanceof Exception ) {
								$results[ "$index" ] = null;
								continue;
							}
							$reports = $values->getReports();
							if ( ! empty( $reports ) ) {
								foreach ( $reports as $report ) {
									$data_items = $report->getData();
									// load details from rows.
									$rows = $data_items->getRows();
									if ( ! empty( $rows ) ) {
										foreach ( $rows as $row ) {
											// if we here - the traffic at page is not empty.
											list( $_slug, $_type ) = $row->getDimensions(); // page slug + traffic type.
											if ( ! isset( $result[ $_slug ] ) ) {
												$result[ $_slug ] = true;
												$result_list[]    = $_slug;
											}
										}
									}

									if ( $return_count ) {
										$count = 0;
										if ( ! empty( $result ) ) {
											$result = array_keys( $result );
											array_walk(
												$result,
												function( $slug ) use ( &$count ) {
													$post = get_page_by_path( "$slug", OBJECT, [ 'post', 'page' ] );
													if ( $post instanceof \WP_Post ) {
														$count++;
													}
												}
											);
										}
										$results[ "$index" ] = $count;
									} else {
										$results[ "$index" ] = $result_list;
									}
								}
							}
						}
					}
				} catch ( Error $e ) {
					$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
					$this->set_message( $message );
				} catch ( Exception $e ) {
					$this->handle_exception( $e, true );

					return $results;
				}
				// load until any next page exists, but load only first page with results for the generic request without page ($page_slugs_list is null).
			} while ( ! empty( $ua_ids ) );
		} catch ( Error $e ) {
			$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
			$this->set_message( $message );
		} finally {
			if ( ! empty( $client ) ) {
				$client->setUseBatch( false );
			}
		}

		return $results;
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
	}

	/**
	 * Check that currently selected GA account has same domain in website property as current site has.
	 * Ignore empty value.
	 *
	 * @param string|null $ua_url Check current GA account if null.
	 *
	 * @return bool|null Null if nothing to check
	 */
	public function is_ga_account_correct( ?string $ua_url = null ) : ?bool {
		if ( is_null( $ua_url ) ) {
			$ua_url = $this->get_data_tokens()->get_ua_url(); // use current account.
		}
		if ( '' === $ua_url ) {
			return null; // nothing to check.
		}
		$domain = strtolower( Ahrefs_Seo::get_current_domain() );
		if ( 0 === strpos( $domain, 'www.' ) ) { // remove www. prefix from domain.
			$domain = substr( $domain, 4 );
		}
		$sites = explode( '|', $ua_url );
		foreach ( $sites as $site_url ) {
			$_website = strtolower( (string) wp_parse_url( $site_url, PHP_URL_HOST ) );
			if ( '' === $_website ) { // incorrect URL, maybe the domain name used here?
				$_website = strtolower( $site_url );
			}
			if ( 0 === strpos( $_website, 'www.' ) ) { // remove www. prefix from domain.
				$_website = substr( $_website, 4 );
			}
			if ( $_website === $domain ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get visitors traffic by type for page for GA4 property.
	 *
	 * @param string[] $ua_ids UA ids list to check.
	 * @param string   $start_date Start date.
	 * @param string   $end_date End date.
	 * @param bool     $return_count Return count of found pages or pages slugs list.
	 *
	 * @return array<string, null|int>|array<string, null|string[]> Array, [ ua_id => pages_found ].
	 * @since 0.7.3
	 *
	 * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing -- we handle exception.
	 */
	public function get_found_pages_by_ua_id_ga4( array $ua_ids, string $start_date, string $end_date, bool $return_count = true ) : array {
		$results = [];
		try {
			$client = $this->create_client();
			$client->setUseBatch( true );

			$analytics4 = new Google_Service_AnalyticsData( $client );

			$per_page = $return_count ? self::QUERY_DETECT_GA_LIMIT : 1000;
			do { // for ua_ids parts.
				$ua_id_list = array_splice( $ua_ids, 0, 5 ); // max 5 requests per batch.
				$result     = [];

				try {
					$data = null;
					// analytics parameters.
					$params = [
						'quotaUser' => $this->get_api_user(),
					];

					// get results from Google Analytics.
					try {
						$this->maybe_do_a_pause( 'ga4' );

						$batch = new Google_Http_Batch(
							$client,
							false,
							$analytics4->rootUrl, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
							$analytics4->batchPath // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
						);
						$this->maybe_do_a_pause( 'ga4', true );

						foreach ( $ua_id_list as $ua_id ) {
							$property_id = str_replace( 'properties/', '', $ua_id );
							// Create the DateRange object.
							$date_range = new Google_Service_AnalyticsData_DateRange();
							$date_range->setStartDate( $start_date );
							$date_range->setEndDate( $end_date );

							// Create the Metrics object.
							/** @link https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema#metrics */
							$metric = new Google_Service_AnalyticsData_Metric();
							$metric->setName( 'screenPageViews' ); // "ga:uniquePageviews"

							// Create the Dimension object.
							/** @link https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema#dimensions */
							$dimension1 = new Google_Service_AnalyticsData_Dimension();
							$dimension1->setName( 'pagePathPlusQueryString' ); // "ga:pagePath".

							// Create the ReportRequest object.
							$request = new RunReportRequest();
							$request->setDateRanges( $date_range );
							$request->setMetrics( array( $metric ) );
							$request->setDimensions( array( $dimension1 ) );
							$request->setLimit( $per_page );
							$request->setOffset( 1 );
							if ( ! $this->set_ga4_property( $request, $property_id ) ) {
								throw new Ahrefs_Seo_Compatibility_Exception( sprintf( 'Unsupported Google Analytics Data API version %s at %s line %d', $analytics4->version, __METHOD__, __LINE__ ) );
							}

							if ( property_exists( $analytics4, 'properties' ) && is_object( $analytics4->properties ) && method_exists( $analytics4->properties, 'runReport' ) ) { // @phpstan-ignore-line -- next version may not have this property.
								$query = $analytics4->properties->runReport( 'properties/' . $property_id, $request, $params );
								$batch->add( $query, 'properties/' . $property_id );
							} else {
								throw new Ahrefs_Seo_Compatibility_Exception( sprintf( 'Unsupported Google Analytics Data API version %s at %s line %d', $analytics4->version, __METHOD__, __LINE__ ) );
							}
						}
						$data = $batch->execute();
					} catch ( Google_Service_Exception $e ) { // try to continue, but report error.
						Ahrefs_Seo_Errors::save_message( 'google', $e->getMessage(), Message::TYPE_NOTICE );
						Ahrefs_Seo::notify( $e, 'autodetect ga4' );
					} catch ( GuzzleConnectException $e ) { // try to continue, but report error.
						Ahrefs_Seo_Errors::save_message( 'google', $e->getMessage(), Message::TYPE_NOTICE );
						Ahrefs_Seo::notify( $e, 'autodetect ga4' );
					}
					if ( ! is_null( $data ) ) {
						foreach ( $data as $index => $report ) {
							$index = str_replace( 'response-', '', $index );
							if ( $report instanceof Exception ) {
								$results[ "$index" ] = null;
								continue;
							}
							$result      = [];
							$result_list = [];
							$rows        = $report->getRows();
							$count       = 0;
							if ( ! empty( $rows ) ) {
								foreach ( $rows as $row ) {
									$dimensions = $row->getDimensionValues(); // page slug.
									$_slug      = $dimensions[0]->getValue();
									if ( ! isset( $result[ $_slug ] ) ) {
										$result[ $_slug ] = true;
										$result_list[]    = $_slug;
									}
								}

								if ( ! empty( $result ) && $return_count ) {
									$result = array_keys( $result );
									array_walk(
										$result,
										function( $slug ) use ( &$count ) {
											$post = get_page_by_path( "$slug", OBJECT, [ 'post', 'page' ] );
											if ( $post instanceof \WP_Post ) {
												$count++;
											}
										}
									);
								}
							}
							$results[ "$index" ] = $return_count ? $count : $result_list;
						}
					}
				} catch ( Error $e ) {
					$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
					$this->set_message( $message );
				} catch ( Exception $e ) {
					$this->handle_exception( $e, true );

					return $results;
				}
				// load until any next page exists, but load only first page with results for the generic request without page ($page_slugs_list is null).
			} while ( ! empty( $ua_ids ) );
		} catch ( Error $e ) {
			$message = Ahrefs_Seo_Compatibility::on_type_error( $e, __METHOD__, __FILE__ );
			$this->set_message( $message );
		} finally {
			if ( ! empty( $client ) ) {
				$client->setUseBatch( false );
			}
		}

		return $results;
	}

	/**
	 * Return number of pages found in GA or GA4 account.
	 *
	 * @param string[] $ua_ids UA ids list.
	 *
	 * @return null|array<string, int|null> Index is ua_id, value is number of found pages.
	 */
	private function check_ga_using_top_traffic_pages( array $ua_ids ) : ?array {
		if ( ! $this->is_analytics_enabled() ) {
			return null;
		}
		$results    = [];
		$start_date = date( 'Y-m-d', time() - 3 * MONTH_IN_SECONDS );
		$end_date   = date( 'Y-m-d' );

		$ua_ids_ga  = [];
		$ua_ids_ga4 = [];

		foreach ( $ua_ids as $ua_id ) {
			if ( 0 === strpos( $ua_id, 'properties/' ) ) {
				$ua_ids_ga4[] = $ua_id;
			} else {
				$ua_ids_ga[] = $ua_id;
			}
		}
		if ( count( $ua_ids_ga ) ) {
			$results = $this->get_found_pages_by_ua_id_ga( $ua_ids_ga, $start_date, $end_date );
		}
		if ( count( $ua_ids_ga4 ) ) {
			$results = $results + $this->get_found_pages_by_ua_id_ga4( $ua_ids_ga4, $start_date, $end_date ); // save indexes.
		}

		return $results;
	}

	/**
	 * Set property id for GA4 request using v1beta API.
	 *
	 * @param RunReportRequest $request Request.
	 * @param string           $property_id Property id.
	 *
	 * @return bool False on error.
	 * @since 0.8.2
	 */
	protected function set_ga4_property( RunReportRequest &$request, string $property_id ) : bool {
		if ( method_exists( $request, 'setProperty' ) ) { // @phpstan-ignore-line -- currently unstable v1beta used, next version may not have this property.
			$request->setProperty( 'properties/' . $property_id );
		} else {
			return false;
		}

		return true;
	}

	/**
	 * Is this a GA4 property?
	 *
	 * @param string $ua_id Identifier to check.
	 *
	 * @return bool
	 * @since 0.9.12
	 */
	private function is_ga4_property( string $ua_id ) : bool {
		return ( 0 === strpos( $ua_id, 'properties/' ) );
	}

	/**
	 * @return int Get number of items in single request.
	 * @since 0.9.12
	 */
	public function get_max_request_items() : int {
		return $this->is_ga4_property( $this->get_data_tokens()->get_ua_id() ) ? self::REQUEST_SIZE_GA4 : self::REQUEST_SIZE_GA;
	}

	/**
	 * Creates ReportRequest instance.
	 *
	 * @param string $start_date Start date.
	 * @param string $end_date End date.
	 * @param string $ua_id UA id.
	 * @param int    $per_page Items count per page.
	 *
	 * @return Google_Service_AnalyticsReporting_ReportRequest
	 */
	private function create_report_request_object( string $start_date, string $end_date, string $ua_id, int $per_page ): Google_Service_AnalyticsReporting_ReportRequest {
		$date_range = new Google_Service_AnalyticsReporting_DateRange();
		$date_range->setStartDate( $start_date );
		$date_range->setEndDate( $end_date );

		// Create the Metrics object.
		$metric1 = new Google_Service_AnalyticsReporting_Metric();
		$metric1->setExpression( 'ga:uniquePageviews' );

		// Create the Dimensions object.
		$dimension1 = new Google_Service_AnalyticsReporting_Dimension();
		$dimension1->setName( 'ga:pagePath' );

		/** @link https://ga-dev-tools.appspot.com/dimensions-metrics-explorer/#ga:channelGrouping */
		$dimension2 = new Google_Service_AnalyticsReporting_Dimension();
		$dimension2->setName( 'ga:channelGrouping' );

		// Create the ReportRequest object.
		$request = new Google_Service_AnalyticsReporting_ReportRequest();

		$request->setViewId( $ua_id );
		$request->setDateRanges( $date_range );
		$request->setDimensions( array( $dimension1, $dimension2 ) );
		$request->setMetrics( array( $metric1 ) );
		$request->setPageSize( $per_page );

		return $request;
	}
}

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists