http = $http; } /** * GA4 properties the account can read, across all accounts. * * @param int $ttl Cache lifetime. * @return array[] Items with id, name and account. * @throws ApiException On failure. */ public function properties( $ttl ) { $out = array(); $token = ''; $pages = 0; do { $url = self::ADMIN . '/accountSummaries?pageSize=200' . ( '' !== $token ? '&pageToken=' . rawurlencode( $token ) : '' ); $data = $this->http->get( $url, $ttl ); foreach ( isset( $data['accountSummaries'] ) ? (array) $data['accountSummaries'] : array() as $account ) { $account_name = isset( $account['displayName'] ) ? $account['displayName'] : ( isset( $account['account'] ) ? $account['account'] : '' ); foreach ( isset( $account['propertySummaries'] ) ? (array) $account['propertySummaries'] : array() as $property ) { if ( empty( $property['property'] ) ) { continue; } $out[] = array( 'id' => (string) $property['property'], 'name' => isset( $property['displayName'] ) ? (string) $property['displayName'] : (string) $property['property'], 'account' => (string) $account_name, ); } } $token = isset( $data['nextPageToken'] ) ? (string) $data['nextPageToken'] : ''; ++$pages; } while ( '' !== $token && $pages < 20 ); return $out; } /** * Event names the property has marked as key events in GA4. * * These are the events GA4 itself treats as conversions, so they are the * ones worth offering first in the site editor. The read-only Analytics * scope the connection already holds covers this, so nothing needs * reconnecting. Properties still on the older "conversion events" surface * answer the second call. * * @param string $property Property resource name, e.g. "properties/123". * @param int $ttl Cache lifetime. * @return string[] Event names, without duplicates. * @throws ApiException On failure or an invalid property. */ public function key_events( $property, $ttl ) { self::check_property( $property ); try { return $this->event_names( $property, 'keyEvents', $ttl ); } catch ( ApiException $e ) { if ( 404 !== $e->status() && 400 !== $e->status() ) { throw $e; } } return $this->event_names( $property, 'conversionEvents', $ttl ); } /** * Reads one of the Admin API's event collections, following its pages. * * @param string $property Property resource name. * @param string $collection keyEvents or conversionEvents. * @param int $ttl Cache lifetime. * @return string[] Event names. * @throws ApiException On failure. */ private function event_names( $property, $collection, $ttl ) { $names = array(); $token = ''; $pages = 0; do { $url = self::ADMIN . '/' . $property . '/' . $collection . '?pageSize=200' . ( '' !== $token ? '&pageToken=' . rawurlencode( $token ) : '' ); $data = $this->http->get( $url, $ttl ); foreach ( isset( $data[ $collection ] ) ? (array) $data[ $collection ] : array() as $event ) { if ( ! empty( $event['eventName'] ) ) { $names[] = (string) $event['eventName']; } } $token = isset( $data['nextPageToken'] ) ? (string) $data['nextPageToken'] : ''; ++$pages; } while ( '' !== $token && $pages < 10 ); return array_values( array_unique( $names ) ); } /** * Runs a GA4 report. * * @param string $property Property resource name, e.g. "properties/123". * @param array $body runReport request body. * @param int $ttl Cache lifetime. * @return array[] Rows as ['d' => dimension values, 'm' => metric values]. * @throws ApiException On failure or an invalid property. */ public function run_report( $property, array $body, $ttl ) { self::check_property( $property ); $data = $this->http->post( self::DATA . '/' . $property . ':runReport', $body, $ttl ); $rows = array(); foreach ( isset( $data['rows'] ) ? (array) $data['rows'] : array() as $row ) { $rows[] = array( 'd' => array_map( static function ( $v ) { return isset( $v['value'] ) ? (string) $v['value'] : ''; }, isset( $row['dimensionValues'] ) ? (array) $row['dimensionValues'] : array() ), 'm' => array_map( static function ( $v ) { return isset( $v['value'] ) ? (float) $v['value'] : 0.0; }, isset( $row['metricValues'] ) ? (array) $row['metricValues'] : array() ), ); } return $rows; } /** * Rejects anything that isn't a GA4 property resource name. * * @param string $property Property resource name. * @return void * @throws ApiException When the name is not a property. */ private static function check_property( $property ) { if ( ! preg_match( '#^properties/\d+$#', $property ) ) { throw new ApiException( __( 'The GA4 property for this site is invalid. Choose it again on the site\'s settings.', 'turtlereport' ), ApiException::NOT_CONFIGURED ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Caught by the plugin; escaped where the message is shown. } } }