diff --git a/includes/Modules/Analytics_4.php b/includes/Modules/Analytics_4.php index 448972490c9..a0384ada847 100644 --- a/includes/Modules/Analytics_4.php +++ b/includes/Modules/Analytics_4.php @@ -92,6 +92,17 @@ final class Analytics_4 extends Module */ const MODULE_SLUG = 'analytics-4'; + /** + * Prefix used to fetch custom dimensions in reports. + */ + const CUSTOM_EVENT_PREFIX = 'customEvent:'; + + /** + * Custom dimensions tracked by Site Kit. + */ + const CUSTOM_DIMENSION_POST_AUTHOR = 'googlesitekit_post_author'; + const CUSTOM_DIMENSION_POST_CATEGORIES = 'googlesitekit_post_categories'; + /** * Registers functionality through WordPress hooks. * diff --git a/includes/Modules/Analytics_4/Report/Custom_Dimensions_Response_Parser.php b/includes/Modules/Analytics_4/Report/Custom_Dimensions_Response_Parser.php new file mode 100644 index 00000000000..974396ad89d --- /dev/null +++ b/includes/Modules/Analytics_4/Report/Custom_Dimensions_Response_Parser.php @@ -0,0 +1,150 @@ + array(), + Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES => array(), + ); + + /** + * Gets the display name for a given user ID. + * + * If no user is found for a given user ID, the original user ID is + * returned. If a user is found, the display name is cached when processing + * the same response. + * + * @since n.e.x.t + * + * @param string|int $user_id User ID of the user to get the display name of. + * @return string|int Display name of the user or their original ID if no name is found. + */ + protected function get_post_author_name( $user_id ) { + if ( ! is_numeric( $user_id ) ) { + return $user_id; + } + + if ( ! isset( $this->cache_map[ Analytics_4::CUSTOM_DIMENSION_POST_AUTHOR ][ $user_id ] ) ) { + $user = get_userdata( $user_id ); + $this->cache_map[ Analytics_4::CUSTOM_DIMENSION_POST_AUTHOR ][ $user_id ] = isset( $user->display_name ) ? $user->display_name : $user_id; + } + + return $this->cache_map[ Analytics_4::CUSTOM_DIMENSION_POST_AUTHOR ][ $user_id ]; + } + + /** + * Converts a string list of category IDs to a stringified array of their + * category names. + * + * If no category is found for a given ID, the original ID is preserved in + * the returned string. + * + * @since n.e.x.t + * + * @param string $category_ids_string Comma separated string list of IDs of categories to get names of. + * @return string JSON encoded string of comma separated category names (or their original IDs if no name is found). + */ + protected function get_post_category_names( $category_ids_string ) { + $category_ids = explode( ',', $category_ids_string ); + + // Explode converts all split values to strings. So we cast any numeric + // strings to `int` so that if a display name is not found for a + // category_id, then the original category_id int can be passed + // through directly in the response. + $category_ids = array_map( + function ( $id ) { + return is_numeric( $id ) ? (int) $id : $id; + }, + $category_ids + ); + + $category_names = array(); + foreach ( $category_ids as $category_id ) { + if ( ! is_numeric( $category_id ) ) { + $category_names[] = $category_id; + continue; + } + + if ( ! isset( $this->cache_map[ Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES ][ $category_id ] ) ) { + $term = get_term( $category_id ); + $this->cache_map[ Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES ][ $category_id ] = isset( $term->name ) ? $term->name : $category_id; + } + + $category_names[] = $this->cache_map[ Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES ][ $category_id ]; + } + + return wp_json_encode( $category_names ); + } + + /** + * Swaps the IDs of any custom dimensions within the response with their respective display names. + * + * @since n.e.x.t + * + * @param Google_Service_AnalyticsData_RunReportResponse $response The response to swap values in. + * @return void Swaps the IDs of custom dimensions within the given response instance. + */ + public function swap_custom_dimension_ids_with_names( $response ) { + if ( $response->getRowCount() === 0 ) { + return; + } + + $dimension_headers = $response->getDimensionHeaders(); + + // Create a map of any custom dimension to its equivalent parsing function to avoid + // looping through report rows multiple times below. + $custom_dimension_map = array(); + foreach ( $dimension_headers as $dimension_key => $dimension ) { + if ( Analytics_4::CUSTOM_EVENT_PREFIX . Analytics_4::CUSTOM_DIMENSION_POST_AUTHOR === $dimension['name'] ) { + $custom_dimension_map[ $dimension_key ] = array( $this, 'get_post_author_name' ); + } + + if ( Analytics_4::CUSTOM_EVENT_PREFIX . Analytics_4::CUSTOM_DIMENSION_POST_CATEGORIES === $dimension['name'] ) { + $custom_dimension_map[ $dimension_key ] = array( $this, 'get_post_category_names' ); + } + } + + if ( empty( $custom_dimension_map ) ) { + return; + } + + $rows = $response->getRows(); + + foreach ( $rows as $row ) { + foreach ( $custom_dimension_map as $dimension_key => $callable ) { + $dimension_value = $row['dimensionValues'][ $dimension_key ]->getValue(); + $new_dimension_value = call_user_func( $callable, $dimension_value ); + $row['dimensionValues'][ $dimension_key ]->setValue( $new_dimension_value ); + } + } + + $response->setRows( $rows ); + } + +} diff --git a/includes/Modules/Analytics_4/Report/Response.php b/includes/Modules/Analytics_4/Report/Response.php index bc84af0f3d0..89ba39ec0d7 100644 --- a/includes/Modules/Analytics_4/Report/Response.php +++ b/includes/Modules/Analytics_4/Report/Response.php @@ -42,6 +42,9 @@ public function parse_response( Data_Request $data, $response ) { return $response; } + $custom_dimension_query = new Custom_Dimensions_Response_Parser(); + $custom_dimension_query->swap_custom_dimension_ids_with_names( $response ); + // Get report dimensions and return early if there is either more than one dimension or // the only dimension is not "date". $dimensions = $this->parse_dimensions( $data ); diff --git a/tests/phpunit/integration/Modules/Analytics_4/Report/Custom_Dimensions_Response_ParserTest.php b/tests/phpunit/integration/Modules/Analytics_4/Report/Custom_Dimensions_Response_ParserTest.php new file mode 100644 index 00000000000..8c055354b0e --- /dev/null +++ b/tests/phpunit/integration/Modules/Analytics_4/Report/Custom_Dimensions_Response_ParserTest.php @@ -0,0 +1,79 @@ + $value ) { + $dimension_value = new Google_Service_AnalyticsData_DimensionValue(); + $dimension_value->setValue( $value ); + $rows[ $key ] = new Google_Service_AnalyticsData_Row(); + $rows[ $key ]->setDimensionValues( array( $dimension_value ) ); + } + return $rows; + } + + public function test_swap_custom_dimension_ids_with_names__post_author() { + $response = new Google_Service_AnalyticsData_RunReportResponse(); + $dimension_header_post_author = new Google_Service_AnalyticsData_DimensionHeader(); + $dimension_header_post_author->setName( 'customEvent:googlesitekit_post_author' ); + $response->setDimensionHeaders( array( $dimension_header_post_author ) ); + + // Existing author with a valid display name. + $author_id = $this->factory()->user->create( array( 'display_name' => 'test author 1' ) ); + + // 5000 would be a non-existent user and `(not set)` is an invalid user_id. + $rows = $this->create_rows_with_dimension_values( array( (string) $author_id, '5000', '(not set)' ) ); + + $response->setRows( $rows ); + $custom_dimension_query = new Custom_Dimensions_Response_Parser(); + $custom_dimension_query->swap_custom_dimension_ids_with_names( $response ); + + // `$rows` would have mutated within $response now having the swapped values. + $this->assertEquals( 'test author 1', $rows[0]->getDimensionValues()[0]->getValue() ); + $this->assertEquals( '5000', $rows[1]->getDimensionValues()[0]->getValue() ); + $this->assertEquals( '(not set)', $rows[2]->getDimensionValues()[0]->getValue() ); + } + + public function test_swap_custom_dimension_ids_with_names__post_categories() { + $response = new Google_Service_AnalyticsData_RunReportResponse(); + $dimension_header_post_categories = new Google_Service_AnalyticsData_DimensionHeader(); + $dimension_header_post_categories->setName( 'customEvent:googlesitekit_post_categories' ); + $response->setDimensionHeaders( array( $dimension_header_post_categories ) ); + + $category_with_number = $this->factory()->category->create( array( 'name' => '2' ) ); + $category_with_commas = $this->factory()->category->create( array( 'name' => 'Category,with,commas' ) ); + $normal_category = $this->factory()->category->create( array( 'name' => 'Normal Category' ) ); + $category_ids_string = implode( ',', array( $category_with_number, $category_with_commas, 1955, 'Uncategorized', $normal_category ) ); // 1955 would be a non-existent category + + $rows = $this->create_rows_with_dimension_values( array( $category_ids_string ) ); + $response->setRows( $rows ); + $custom_dimension_query = new Custom_Dimensions_Response_Parser(); + $custom_dimension_query->swap_custom_dimension_ids_with_names( $response ); + + // `$rows` would have mutated within $response now having the swapped values. + $this->assertEquals( '["2","Category,with,commas",1955,"Uncategorized","Normal Category"]', $rows[0]->getDimensionValues()[0]->getValue() ); + } +}