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 */ public function load_accounts_list() : array { $result = []; try { $ga4 = ( defined( 'AHREFS_SEO_NO_GA' ) && AHREFS_SEO_NO_GA ) ? [ [ 'ua_id' => 'AHREFS_SEO_NO_GA', 'account' => 'AHREFS_SEO_NO_GA', 'account_name' => 'AHREFS_SEO_NO_GA', 'name' => 'AHREFS_SEO_NO_GA', 'stream' => Ahrefs_Seo::get_current_domain(), 'website' => 'https://' . Ahrefs_Seo::get_current_domain(), ], ] : $this->load_accounts_list_ga4(); // sort results. usort( $ga4, 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 ( $ga4 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 * * @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 } /** * Get visitors traffic by type for page * * @param array|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>|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 $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 ) : null; // deprecated Universal Analytics property. // 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 $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> Array, 'slug' => [ traffic type => visitors number] * @phpstan-return array|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 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; } $result = []; if ( 0 === strpos( $ua_id, 'properties/' ) ) { $result = $this->get_found_pages_by_ua_id_ga4( [ $ua_id ], $start_date, $end_date, false ); } $item = array_shift( $result ); return is_array( $item ) ? $item : null; } /** * 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|array 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 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_ga4 = []; foreach ( $ua_ids as $ua_id ) { if ( 0 === strpos( $ua_id, 'properties/' ) ) { $ua_ids_ga4[] = $ua_id; } } 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; } }