From c14b6dfbf4d3ef7cbcf9f8850d21a4e109c85181 Mon Sep 17 00:00:00 2001 From: Abdelmalek El Mellouki Date: Wed, 25 Mar 2026 15:19:47 +0100 Subject: [PATCH 1/8] Extract get report datapoint. --- includes/Core/Modules/Module.php | 33 +++-- includes/Modules/Analytics_4.php | 49 ++----- .../Analytics_4/Datapoints/Get_Report.php | 122 ++++++++++++++++++ 3 files changed, 155 insertions(+), 49 deletions(-) create mode 100644 includes/Modules/Analytics_4/Datapoints/Get_Report.php diff --git a/includes/Core/Modules/Module.php b/includes/Core/Modules/Module.php index 1b2e5e843f6..c942adfb251 100644 --- a/includes/Core/Modules/Module.php +++ b/includes/Core/Modules/Module.php @@ -502,14 +502,7 @@ final public function get_client() { * @return OAuth_Client OAuth_Client instance. */ private function get_oauth_client_for_datapoint( Datapoint $datapoint ) { - if ( - $this instanceof Module_With_Owner - && $this->is_shareable() - && $datapoint->is_shareable() - && $this->get_owner_id() !== get_current_user_id() - && ! $this->is_recoverable() - && current_user_can( Permissions::READ_SHARED_MODULE_DATA, $this->slug ) - ) { + if ( $this instanceof Module_With_Owner && $this->is_shared_datapoint_request( $datapoint ) ) { $oauth_client = $this->get_owner_oauth_client(); try { @@ -753,14 +746,26 @@ function ( $item ) { * @return bool TRUE if the request is for shared data, otherwise FALSE. */ protected function is_shared_data_request( Data_Request $data ) { - $datapoint = $this->get_datapoint_definition( "{$data->method}:{$data->datapoint}" ); - $oauth_client = $this->get_oauth_client_for_datapoint( $datapoint ); + $datapoint = $this->get_datapoint_definition( "{$data->method}:{$data->datapoint}" ); - if ( $this->authentication->get_oauth_client() !== $oauth_client ) { - return true; - } + return $this->is_shared_datapoint_request( $datapoint ); + } - return false; + /** + * Determines whether the current datapoint request is for shared data. + * + * @since n.e.x.t + * + * @param Datapoint $datapoint Datapoint instance. + * @return bool TRUE if the request is for shared data, otherwise FALSE. + */ + protected function is_shared_datapoint_request( Datapoint $datapoint ) { + return $this instanceof Module_With_Owner + && $this->is_shareable() + && $datapoint->is_shareable() + && $this->get_owner_id() !== get_current_user_id() + && ! $this->is_recoverable() + && current_user_can( Permissions::READ_SHARED_MODULE_DATA, $this->slug ); } /** diff --git a/includes/Modules/Analytics_4.php b/includes/Modules/Analytics_4.php index c8c1c306d3c..0385db75862 100644 --- a/includes/Modules/Analytics_4.php +++ b/includes/Modules/Analytics_4.php @@ -78,6 +78,7 @@ use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Webdatastreams_Batch; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Sync_Audiences; +use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Report; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Custom_Dimension_Data_Available; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Resource_Data_Availability_Date; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Set_Google_Tag_ID_Mismatch; @@ -118,6 +119,7 @@ use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_New_Badge_Events_Sync; use Google\Site_Kit\Modules\Analytics_4\Conversion_Reporting\Conversion_Reporting_Provider; use Google\Site_Kit\Modules\Analytics_4\Reset_Audiences; +use Google\Site_Kit\Core\Modules\Datapoint; use stdClass; use WP_Error; use WP_Post; @@ -778,9 +780,18 @@ protected function get_datapoint_definitions() { 'GET:properties' => array( 'service' => 'analyticsadmin' ), 'GET:property' => array( 'service' => 'analyticsadmin' ), 'GET:has-property-access' => array( 'service' => 'analyticsdata' ), - 'GET:report' => array( - 'service' => 'analyticsdata', - 'shareable' => true, + 'GET:report' => new Get_Report( + array( + 'service' => function () { + return $this->get_service( 'analyticsdata' ); + }, + 'shareable' => true, + 'settings' => $this->get_settings(), + 'context' => $this->context, + 'is_shared_request' => function ( Datapoint $datapoint ) { + return $this->is_shared_datapoint_request( $datapoint ); + }, + ), ), 'GET:batch-report' => array( 'service' => 'analyticsdata', @@ -1318,35 +1329,6 @@ protected function create_data_request( Data_Request $data ) { $request->setLimit( 0 ); return $this->get_analyticsdata_service()->properties->runReport( $data['propertyID'], $request ); - case 'GET:report': - if ( empty( $data['metrics'] ) ) { - return new WP_Error( - 'missing_required_param', - /* translators: %s: Missing parameter name */ - sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'metrics' ), - array( 'status' => 400 ) - ); - } - - $settings = $this->get_settings()->get(); - if ( empty( $settings['propertyID'] ) ) { - return new WP_Error( - 'missing_required_setting', - __( 'No connected Google Analytics property ID.', 'google-site-kit' ), - array( 'status' => 500 ) - ); - } - - $report = new Analytics_4_Report_Request( $this->context ); - $request = $report->create_request( $data, $this->is_shared_data_request( $data ) ); - if ( is_wp_error( $request ) ) { - return $request; - } - - $property_id = self::normalize_property_id( $settings['propertyID'] ); - $request->setProperty( $property_id ); - - return $this->get_analyticsdata_service()->properties->runReport( $property_id, $request ); case 'GET:batch-report': if ( empty( $data['requests'] ) ) { @@ -1443,9 +1425,6 @@ protected function parse_data_response( Data_Request $data, $response ) { return self::filter_property_with_ids( $response ); case 'GET:key-events': return (array) $response->getKeyEvents(); - case 'GET:report': - $report = new Analytics_4_Report_Response( $this->context ); - return $report->parse_response( $data, $response ); } return parent::parse_data_response( $data, $response ); diff --git a/includes/Modules/Analytics_4/Datapoints/Get_Report.php b/includes/Modules/Analytics_4/Datapoints/Get_Report.php new file mode 100644 index 00000000000..7418597429f --- /dev/null +++ b/includes/Modules/Analytics_4/Datapoints/Get_Report.php @@ -0,0 +1,122 @@ +settings = $definition['settings']; + $this->context = $definition['context']; + $this->is_shared_request = $definition['is_shared_request']; + } + + /** + * Creates a request object. + * + * @since n.e.x.t + * + * @param Data_Request $data Data request object. + */ + public function create_request( Data_Request $data ) { + if ( empty( $data['metrics'] ) ) { + return new WP_Error( + 'missing_required_param', + /* translators: %s: Missing parameter name */ + sprintf( __( 'Request parameter is empty: %s.', 'google-site-kit' ), 'metrics' ), + array( 'status' => 400 ) + ); + } + + $settings = $this->settings->get(); + + if ( empty( $settings['propertyID'] ) ) { + return new WP_Error( + 'missing_required_setting', + __( 'No connected Google Analytics property ID.', 'google-site-kit' ), + array( 'status' => 500 ) + ); + } + + $report = new Analytics_4_Report_Request( $this->context ); + $request = $report->create_request( $data, ( $this->is_shared_request )( $this ) ); + if ( is_wp_error( $request ) ) { + return $request; + } + + $property_id = Analytics_4::normalize_property_id( $settings['propertyID'] ); + $request->setProperty( $property_id ); + + return $this->get_service()->properties->runReport( $property_id, $request ); + } + + /** + * Parses a response. + * + * @since n.e.x.t + * + * @param mixed $response Request response. + * @param Data_Request $data Data request object. + * @return mixed Parsed response data on success, or WP_Error on failure. + */ + public function parse_response( $response, Data_Request $data ) { + $report = new Analytics_4_Report_Response( $this->context ); + return $report->parse_response( $data, $response ); + } +} From 55bb1ccc35456790f6ed53ee11e115bb7bb4efee Mon Sep 17 00:00:00 2001 From: Abdelmalek El Mellouki Date: Wed, 25 Mar 2026 20:50:39 +0100 Subject: [PATCH 2/8] Add get report datapoint tests. --- .../Analytics_4/Datapoints/Get_ReportTest.php | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php new file mode 100644 index 00000000000..544cb7bb174 --- /dev/null +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php @@ -0,0 +1,179 @@ +factory()->user->create_and_get( array( 'role' => 'administrator' ) ); + $user_options = new User_Options( $context, $user->ID ); + $authentication = new Authentication( $context, $options, $user_options ); + $this->analytics = new Analytics_4( $context, $options, $user_options, $authentication ); + + $reflection = new \ReflectionClass( $this->analytics ); + $is_shared_request_method = $reflection->getMethod( 'is_shared_datapoint_request' ); + $is_shared_request_method->setAccessible( true ); + + $this->analytics->get_client()->withDefer( true ); + $service = new Google_Service_AnalyticsData( $this->analytics->get_client() ); + + $this->reportResponse = new ReportResponse( $context ); + + $this->datapoint = new Get_Report( + array( + 'service' => function () use ( $service ) { + return $service; + }, + 'shareable' => true, + 'settings' => $this->analytics->get_settings(), + 'context' => $context, + 'is_shared_request' => function ( $datapoint ) use ( $is_shared_request_method ) { + return $is_shared_request_method->invoke( $this->analytics, $datapoint ); + }, + ), + ); + + FakeHttp::fake_google_http_handler( + $this->analytics->get_client(), + function ( Request $request ) { + $this->get_report_request = $request; + + $response = new Google_Service_AnalyticsData_RunReportResponse(); + + return new FulfilledPromise( new Response( 200, array(), json_encode( $response ) ) ); + } + ); + + wp_set_current_user( $user->ID ); + } + + public function test_create_request__requires_metrics_param() { + $data = array(); + + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'report', $data ); + + $request = $this->datapoint->create_request( $data_request ); + + $this->assertInstanceOf( WP_Error::class, $request, 'The datapoint should return an error when the `metrics` parameter is missing.' ); + + $this->assertEquals( 'missing_required_param', $request->get_error_code(), 'The datapoint should return a `missing_required_param` error when the `metrics` parameter is missing.' ); + } + + public function test_create_request__requires_property_id() { + $data = array( + 'metrics' => array( 'sessions' ), + ); + + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'report', $data ); + + $request = $this->datapoint->create_request( $data_request ); + + $this->assertInstanceOf( WP_Error::class, $request, 'The datapoint should return an error when the `propertyID` setting is missing.' ); + + $this->assertEquals( 'missing_required_setting', $request->get_error_code(), 'The datapoint should return a `missing_required_setting` error when the `propertyID` setting is missing.' ); + } + + public function test_create_request() { + $this->get_report_request = null; + + $this->analytics->get_settings()->merge( + array( + 'propertyID' => '12345', + ) + ); + + $data = array( + 'metrics' => array( 'sessions' ), + ); + + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'report', $data ); + + $request = $this->datapoint->create_request( $data_request ); + + $result = $this->analytics->get_client()->execute( $request ); + + $this->assertEquals( + 'https://analyticsdata.googleapis.com/v1beta/properties/12345:runReport', + $this->get_report_request->getUri(), + 'The request should be made to the correct endpoint.' + ); + } + + public function test_parse_response() { + $data = array( + 'metrics' => array( 'sessions' ), + ); + + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'report', $data ); + + $response = $this->datapoint->parse_response( new Google_Service_AnalyticsData_RunReportResponse(), $data_request ); + + $this->assertEquals( + $response, + $this->reportResponse->parse_response( $data_request, new Google_Service_AnalyticsData_RunReportResponse() ), + 'The datapoint should parse the response using Analytics_4\Report\Response.' + ); + } +} From 5aedaf31f299c829403ef5fb5983b83301d7b806 Mon Sep 17 00:00:00 2001 From: Abdelmalek El Mellouki Date: Thu, 26 Mar 2026 12:23:06 +0100 Subject: [PATCH 3/8] Extract has property access datapoint. --- includes/Modules/Analytics_4.php | 32 ++------ .../Datapoints/Get_Has_Property_Access.php | 77 +++++++++++++++++++ .../Analytics_4/Datapoints/Get_Report.php | 2 + 3 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 includes/Modules/Analytics_4/Datapoints/Get_Has_Property_Access.php diff --git a/includes/Modules/Analytics_4.php b/includes/Modules/Analytics_4.php index 0385db75862..61c84e36520 100644 --- a/includes/Modules/Analytics_4.php +++ b/includes/Modules/Analytics_4.php @@ -79,6 +79,7 @@ use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Sync_Audiences; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Report; +use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Has_Property_Access; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Custom_Dimension_Data_Available; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Resource_Data_Availability_Date; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Set_Google_Tag_ID_Mismatch; @@ -779,7 +780,13 @@ protected function get_datapoint_definitions() { ), 'GET:properties' => array( 'service' => 'analyticsadmin' ), 'GET:property' => array( 'service' => 'analyticsadmin' ), - 'GET:has-property-access' => array( 'service' => 'analyticsdata' ), + 'GET:has-property-access' => new Get_Has_Property_Access( + array( + 'service' => function () { + return $this->get_service( 'analyticsdata' ); + }, + ) + ), 'GET:report' => new Get_Report( array( 'service' => function () { @@ -1306,29 +1313,6 @@ protected function create_data_request( Data_Request $data ) { } return $this->get_service( 'analyticsadmin' )->properties->get( self::normalize_property_id( $data['propertyID'] ) ); - case 'GET:has-property-access': - if ( ! isset( $data['propertyID'] ) ) { - throw new Missing_Required_Param_Exception( 'propertyID' ); - } - - // A simple way to check for property access is to attempt a minimal report request. - // If the user does not have access, this will return a 403 error. - $request = new Google_Service_AnalyticsData_RunReportRequest(); - $request->setDimensions( array( new Google_Service_AnalyticsData_Dimension( array( 'name' => 'date' ) ) ) ); - $request->setMetrics( array( new Google_Service_AnalyticsData_Metric( array( 'name' => 'sessions' ) ) ) ); - $request->setDateRanges( - array( - new Google_Service_AnalyticsData_DateRange( - array( - 'start_date' => 'yesterday', - 'end_date' => 'today', - ) - ), - ) - ); - $request->setLimit( 0 ); - - return $this->get_analyticsdata_service()->properties->runReport( $data['propertyID'], $request ); case 'GET:batch-report': if ( empty( $data['requests'] ) ) { diff --git a/includes/Modules/Analytics_4/Datapoints/Get_Has_Property_Access.php b/includes/Modules/Analytics_4/Datapoints/Get_Has_Property_Access.php new file mode 100644 index 00000000000..d6f98f80e07 --- /dev/null +++ b/includes/Modules/Analytics_4/Datapoints/Get_Has_Property_Access.php @@ -0,0 +1,77 @@ +setDimensions( array( new Google_Service_AnalyticsData_Dimension( array( 'name' => 'date' ) ) ) ); + $request->setMetrics( array( new Google_Service_AnalyticsData_Metric( array( 'name' => 'sessions' ) ) ) ); + $request->setDateRanges( + array( + new Google_Service_AnalyticsData_DateRange( + array( + 'start_date' => 'yesterday', + 'end_date' => 'today', + ) + ), + ) + ); + $request->setLimit( 0 ); + + return $this->get_service()->properties->runReport( $data['propertyID'], $request ); + } + + /** + * Parses a response. + * + * @since n.e.x.t + * + * @param mixed $response Request response. + * @param Data_Request $data Data request object. + * @return mixed The original response without any modifications. + */ + public function parse_response( $response, Data_Request $data ) { + return $response; + } +} diff --git a/includes/Modules/Analytics_4/Datapoints/Get_Report.php b/includes/Modules/Analytics_4/Datapoints/Get_Report.php index 7418597429f..87658061d90 100644 --- a/includes/Modules/Analytics_4/Datapoints/Get_Report.php +++ b/includes/Modules/Analytics_4/Datapoints/Get_Report.php @@ -18,6 +18,7 @@ use Google\Site_Kit\Modules\Analytics_4\Report\Request as Analytics_4_Report_Request; use Google\Site_Kit\Modules\Analytics_4\Report\Response as Analytics_4_Report_Response; use Google\Site_Kit\Modules\Analytics_4\Settings; +use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse as Google_Service_AnalyticsData_RunReportResponse; use WP_Error; /** @@ -73,6 +74,7 @@ public function __construct( array $definition ) { * @since n.e.x.t * * @param Data_Request $data Data request object. + * @return mixed Request object on success, or WP_Error on failure. */ public function create_request( Data_Request $data ) { if ( empty( $data['metrics'] ) ) { From c830beaa21cf4797d87b066e9f973ec758496b00 Mon Sep 17 00:00:00 2001 From: Abdelmalek El Mellouki Date: Thu, 26 Mar 2026 13:28:36 +0100 Subject: [PATCH 4/8] Add tests for has property access datapoint. --- .../Analytics_4/Datapoints/Get_Report.php | 1 - .../Get_Has_Property_AccessTest.php | 144 ++++++++++++++++++ .../Analytics_4/Datapoints/Get_ReportTest.php | 10 +- 3 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php diff --git a/includes/Modules/Analytics_4/Datapoints/Get_Report.php b/includes/Modules/Analytics_4/Datapoints/Get_Report.php index 87658061d90..a3f0bd809fe 100644 --- a/includes/Modules/Analytics_4/Datapoints/Get_Report.php +++ b/includes/Modules/Analytics_4/Datapoints/Get_Report.php @@ -18,7 +18,6 @@ use Google\Site_Kit\Modules\Analytics_4\Report\Request as Analytics_4_Report_Request; use Google\Site_Kit\Modules\Analytics_4\Report\Response as Analytics_4_Report_Response; use Google\Site_Kit\Modules\Analytics_4\Settings; -use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse as Google_Service_AnalyticsData_RunReportResponse; use WP_Error; /** diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php new file mode 100644 index 00000000000..8b5ddd03039 --- /dev/null +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php @@ -0,0 +1,144 @@ +factory()->user->create_and_get( array( 'role' => 'administrator' ) ); + $user_options = new User_Options( $context, $user->ID ); + $authentication = new Authentication( $context, $options, $user_options ); + $this->analytics = new Analytics_4( $context, $options, $user_options, $authentication ); + + $this->analytics->get_client()->withDefer( true ); + + $service = new Google_Service_AnalyticsData( $this->analytics->get_client() ); + + $this->datapoint = new Get_Has_Property_Access( + array( + 'service' => function () use ( $service ) { + return $service; + }, + ), + ); + + FakeHttp::fake_google_http_handler( + $this->analytics->get_client(), + function ( Request $request ) { + $this->get_report_request = $request; + + $response = new Google_Service_AnalyticsData_RunReportResponse(); + + return new FulfilledPromise( new Response( 200, array(), json_encode( $response ) ) ); + } + ); + + wp_set_current_user( $user->ID ); + } + + public function test_create_request__requires_property_id_param() { + $data = array(); + + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'has-property-access', $data ); + + try { + $this->datapoint->create_request( $data_request ); + $this->fail( 'Expected Missing_Required_Param_Exception to be thrown.' ); + } catch ( \Exception $e ) { + $this->assertInstanceOf( Missing_Required_Param_Exception::class, $e, 'The datapoint should throw Missing_Required_Param_Exception when the `propertyID` parameter is missing.' ); + } + } + + + public function test_create_request() { + $this->get_report_request = null; + + $data = array( + 'propertyID' => '12345', + ); + + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'has-property-access', $data ); + + $request = $this->datapoint->create_request( $data_request ); + + $result = $this->analytics->get_client()->execute( $request ); + + $this->assertEquals( + 'https://analyticsdata.googleapis.com/v1beta/12345:runReport', + $this->get_report_request->getUri(), + 'The request should be made to the correct endpoint.' + ); + } + + public function test_parse_response() { + $data = array( + 'metrics' => array( 'sessions' ), + ); + + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'report', $data ); + + $report_response = new Google_Service_AnalyticsData_RunReportResponse(); + $response = $this->datapoint->parse_response( $report_response, $data_request ); + + $this->assertSame( + $response, + $this->reportResponse->parse_response( $data_request, $report_response ), + 'The datapoint should return the response as it is, without any modifications.' + ); + } +} diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php index 544cb7bb174..e63556f567a 100644 --- a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php @@ -42,7 +42,7 @@ class Get_ReportTest extends TestCase { private $datapoint; /** - * Provision account ticket request instance. + * Get report request instance. * * @var Request */ @@ -72,10 +72,6 @@ public function set_up() { $authentication = new Authentication( $context, $options, $user_options ); $this->analytics = new Analytics_4( $context, $options, $user_options, $authentication ); - $reflection = new \ReflectionClass( $this->analytics ); - $is_shared_request_method = $reflection->getMethod( 'is_shared_datapoint_request' ); - $is_shared_request_method->setAccessible( true ); - $this->analytics->get_client()->withDefer( true ); $service = new Google_Service_AnalyticsData( $this->analytics->get_client() ); @@ -89,8 +85,8 @@ public function set_up() { 'shareable' => true, 'settings' => $this->analytics->get_settings(), 'context' => $context, - 'is_shared_request' => function ( $datapoint ) use ( $is_shared_request_method ) { - return $is_shared_request_method->invoke( $this->analytics, $datapoint ); + 'is_shared_request' => function () { + return false; }, ), ); From 7929c6bb0f8f921be15be7accb0ad0f6da2e41a8 Mon Sep 17 00:00:00 2001 From: Abdelmalek El Mellouki Date: Thu, 26 Mar 2026 14:25:17 +0100 Subject: [PATCH 5/8] Extract get key events datapoint. --- includes/Modules/Analytics_4.php | 35 +---- .../Analytics_4/Datapoints/Get_Key_Events.php | 88 +++++++++++ .../Get_Has_Property_AccessTest.php | 8 +- .../Datapoints/Get_Key_EventsTest.php | 148 ++++++++++++++++++ 4 files changed, 246 insertions(+), 33 deletions(-) create mode 100644 includes/Modules/Analytics_4/Datapoints/Get_Key_Events.php create mode 100644 tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php diff --git a/includes/Modules/Analytics_4.php b/includes/Modules/Analytics_4.php index 61c84e36520..0ddf7e844e3 100644 --- a/includes/Modules/Analytics_4.php +++ b/includes/Modules/Analytics_4.php @@ -79,6 +79,7 @@ use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Sync_Audiences; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Report; +use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Key_Events; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Has_Property_Access; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Custom_Dimension_Data_Available; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Resource_Data_Availability_Date; @@ -90,7 +91,6 @@ use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdSenseLinked; use Google\Site_Kit\Modules\Analytics_4\GoogleAnalyticsAdmin\AccountProvisioningService; use Google\Site_Kit\Modules\Analytics_4\Report\Request as Analytics_4_Report_Request; -use Google\Site_Kit\Modules\Analytics_4\Report\Response as Analytics_4_Report_Response; use Google\Site_Kit\Modules\Analytics_4\Resource_Data_Availability_Date; use Google\Site_Kit\Modules\Analytics_4\Settings; use Google\Site_Kit\Modules\Analytics_4\Synchronize_AdsLinked; @@ -99,10 +99,6 @@ use Google\Site_Kit\Modules\Analytics_4\Web_Tag; use Google\Site_Kit_Dependencies\Google\Model as Google_Model; use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData as Google_Service_AnalyticsData; -use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest; -use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DateRange as Google_Service_AnalyticsData_DateRange; -use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Dimension as Google_Service_AnalyticsData_Dimension; -use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Metric as Google_Service_AnalyticsData_Metric; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin as Google_Service_GoogleAnalyticsAdmin; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStream; use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStreamWebStreamData; @@ -733,9 +729,13 @@ protected function get_datapoint_definitions() { ), ) ), - 'GET:key-events' => array( - 'service' => 'analyticsadmin', - 'shareable' => true, + 'GET:key-events' => new Get_Key_Events( + array( + 'service' => function () { + return $this->get_service( 'analyticsadmin' ); + }, + 'settings' => $this->get_settings(), + ) ), 'POST:create-account-ticket' => new Create_Account_Ticket( array( @@ -792,7 +792,6 @@ protected function get_datapoint_definitions() { 'service' => function () { return $this->get_service( 'analyticsdata' ); }, - 'shareable' => true, 'settings' => $this->get_settings(), 'context' => $this->context, 'is_shared_request' => function ( Datapoint $datapoint ) { @@ -1365,22 +1364,6 @@ protected function create_data_request( Data_Request $data ) { $property_id, $batch_request ); - case 'GET:key-events': - $settings = $this->get_settings()->get(); - if ( empty( $settings['propertyID'] ) ) { - return new WP_Error( - 'missing_required_setting', - __( 'No connected Google Analytics property ID.', 'google-site-kit' ), - array( 'status' => 500 ) - ); - } - - $analyticsadmin = $this->get_service( 'analyticsadmin' ); - $property_id = self::normalize_property_id( $settings['propertyID'] ); - - return $analyticsadmin - ->properties_keyEvents // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - ->listPropertiesKeyEvents( $property_id ); } return parent::create_data_request( $data ); @@ -1407,8 +1390,6 @@ protected function parse_data_response( Data_Request $data, $response ) { ); case 'GET:property': return self::filter_property_with_ids( $response ); - case 'GET:key-events': - return (array) $response->getKeyEvents(); } return parent::parse_data_response( $data, $response ); diff --git a/includes/Modules/Analytics_4/Datapoints/Get_Key_Events.php b/includes/Modules/Analytics_4/Datapoints/Get_Key_Events.php new file mode 100644 index 00000000000..69c73fd7c4c --- /dev/null +++ b/includes/Modules/Analytics_4/Datapoints/Get_Key_Events.php @@ -0,0 +1,88 @@ +settings = $definition['settings']; + } + + /** + * Creates a request object. + * + * @since n.e.x.t + * + * @param Data_Request $data Data request object. + * @return mixed Request object on success, or WP_Error on failure. + */ + public function create_request( Data_Request $data ) { + $settings = $this->settings->get(); + + if ( empty( $settings['propertyID'] ) ) { + return new WP_Error( + 'missing_required_setting', + __( 'No connected Google Analytics property ID.', 'google-site-kit' ), + array( 'status' => 500 ) + ); + } + + $property_id = Analytics_4::normalize_property_id( $settings['propertyID'] ); + + return $this->get_service() + ->properties_keyEvents // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + ->listPropertiesKeyEvents( $property_id ); + } + + /** + * Parses a response. + * + * @since n.e.x.t + * + * @param mixed $response Request response. + * @param Data_Request $data Data request object. + * @return GoogleAnalyticsAdminV1betaKeyEvent[] Array of key events. + */ + public function parse_response( $response, Data_Request $data ) { + return (array) $response->getKeyEvents(); + } +} diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php index 8b5ddd03039..9090947327d 100644 --- a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php @@ -126,18 +126,14 @@ public function test_create_request() { } public function test_parse_response() { - $data = array( - 'metrics' => array( 'sessions' ), - ); - - $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'report', $data ); + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'has-property-access' ); $report_response = new Google_Service_AnalyticsData_RunReportResponse(); $response = $this->datapoint->parse_response( $report_response, $data_request ); $this->assertSame( $response, - $this->reportResponse->parse_response( $data_request, $report_response ), + $report_response, 'The datapoint should return the response as it is, without any modifications.' ); } diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php new file mode 100644 index 00000000000..bd47a22438e --- /dev/null +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php @@ -0,0 +1,148 @@ +factory()->user->create_and_get( array( 'role' => 'administrator' ) ); + $user_options = new User_Options( $context, $user->ID ); + $authentication = new Authentication( $context, $options, $user_options ); + $this->analytics = new Analytics_4( $context, $options, $user_options, $authentication ); + + $this->analytics->get_client()->withDefer( true ); + $service = new Google_Service_GoogleAnalyticsAdmin( $this->analytics->get_client() ); + + $this->datapoint = new Get_Key_Events( + array( + 'service' => function () use ( $service ) { + return $service; + }, + 'settings' => $this->analytics->get_settings(), + ), + ); + + FakeHttp::fake_google_http_handler( + $this->analytics->get_client(), + function ( Request $request ) { + $this->get_key_events_request = $request; + + $response = new GoogleAnalyticsAdminV1betaListKeyEventsResponse(); + $response->setKeyEvents( array( new GoogleAnalyticsAdminV1betaKeyEvent() ) ); + + return new FulfilledPromise( + new Response( + 200, + array(), + json_encode( $response ) + ) + ); + } + ); + + wp_set_current_user( $user->ID ); + } + + public function test_create_request__requires_property_id() { + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'key-events' ); + + $request = $this->datapoint->create_request( $data_request ); + + $this->assertInstanceOf( WP_Error::class, $request, 'The datapoint should return an error when the `propertyID` setting is missing.' ); + + $this->assertEquals( 'missing_required_setting', $request->get_error_code(), 'The datapoint should return a `missing_required_setting` error when the `propertyID` setting is missing.' ); + } + + public function test_create_request() { + $this->get_key_events_request = null; + + $this->analytics->get_settings()->merge( + array( + 'propertyID' => '12345', + ) + ); + + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'key-events' ); + + $request = $this->datapoint->create_request( $data_request ); + + $result = $this->analytics->get_client()->execute( $request ); + + $this->assertEquals( + 'https://analyticsadmin.googleapis.com/v1beta/properties/12345/keyEvents', + $this->get_key_events_request->getUri(), + 'The request should be made to the correct endpoint.' + ); + } + + public function test_parse_response() { + $data_request = new Data_Request( 'GET', 'modules', 'analytics-4', 'key-events' ); + + $key_events_response = new GoogleAnalyticsAdminV1betaListKeyEventsResponse(); + $key_events_response->setKeyEvents( array( new GoogleAnalyticsAdminV1betaKeyEvent() ) ); + + $response = $this->datapoint->parse_response( $key_events_response, $data_request ); + + $this->assertEquals( + $response, + $key_events_response->getKeyEvents(), + 'The datapoint should return an array of events.' + ); + } +} From 79f49542a2054f55ec35092cd6f6f4920b2af6af Mon Sep 17 00:00:00 2001 From: Abdelmalek El Mellouki Date: Thu, 26 Mar 2026 15:00:52 +0100 Subject: [PATCH 6/8] Improve import name. --- .../Modules/Analytics_4/Datapoints/Get_ReportTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php index e63556f567a..daf69a4f6e6 100644 --- a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php @@ -12,7 +12,7 @@ use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Report; -use Google\Site_Kit\Modules\Analytics_4\Report\Response as ReportResponse; +use Google\Site_Kit\Modules\Analytics_4\Report\Response as Analytics_4_Report_Response; use Google\Site_Kit\Tests\TestCase; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Authentication\Authentication; @@ -56,9 +56,9 @@ class Get_ReportTest extends TestCase { private $analytics; /** - * Report\Response instance. + * Analytics_4_Report_Response instance. * - * @var ReportRespose + * @var Analytics_4_Report_Response */ private $reportResponse; @@ -75,7 +75,7 @@ public function set_up() { $this->analytics->get_client()->withDefer( true ); $service = new Google_Service_AnalyticsData( $this->analytics->get_client() ); - $this->reportResponse = new ReportResponse( $context ); + $this->reportResponse = new Analytics_4_Report_Response( $context ); $this->datapoint = new Get_Report( array( @@ -169,7 +169,7 @@ public function test_parse_response() { $this->assertEquals( $response, $this->reportResponse->parse_response( $data_request, new Google_Service_AnalyticsData_RunReportResponse() ), - 'The datapoint should parse the response using Analytics_4\Report\Response.' + 'The datapoint should parse the response using Analytics_4_Report_Response.' ); } } From d38443ac1ee9450b32bf2884c4d710a2fade45f3 Mon Sep 17 00:00:00 2001 From: Abdelmalek El Mellouki Date: Tue, 7 Apr 2026 12:19:42 +0100 Subject: [PATCH 7/8] Fix is_shared_datapoint_request logic. --- includes/Core/Modules/Module.php | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/includes/Core/Modules/Module.php b/includes/Core/Modules/Module.php index c942adfb251..1ca77051ae2 100644 --- a/includes/Core/Modules/Module.php +++ b/includes/Core/Modules/Module.php @@ -502,7 +502,14 @@ final public function get_client() { * @return OAuth_Client OAuth_Client instance. */ private function get_oauth_client_for_datapoint( Datapoint $datapoint ) { - if ( $this instanceof Module_With_Owner && $this->is_shared_datapoint_request( $datapoint ) ) { + if ( + $this instanceof Module_With_Owner + && $this->is_shareable() + && $datapoint->is_shareable() + && $this->get_owner_id() !== get_current_user_id() + && ! $this->is_recoverable() + && current_user_can( Permissions::READ_SHARED_MODULE_DATA, $this->slug ) + ) { $oauth_client = $this->get_owner_oauth_client(); try { @@ -760,12 +767,13 @@ protected function is_shared_data_request( Data_Request $data ) { * @return bool TRUE if the request is for shared data, otherwise FALSE. */ protected function is_shared_datapoint_request( Datapoint $datapoint ) { - return $this instanceof Module_With_Owner - && $this->is_shareable() - && $datapoint->is_shareable() - && $this->get_owner_id() !== get_current_user_id() - && ! $this->is_recoverable() - && current_user_can( Permissions::READ_SHARED_MODULE_DATA, $this->slug ); + $oauth_client = $this->get_oauth_client_for_datapoint( $datapoint ); + + if ( $this->authentication->get_oauth_client() !== $oauth_client ) { + return true; + } + + return false; } /** From 5ab1bf90379550914c387e617d8066c8f33180c2 Mon Sep 17 00:00:00 2001 From: Abdelmalek El Mellouki Date: Tue, 7 Apr 2026 12:25:47 +0100 Subject: [PATCH 8/8] Address review feedback. --- includes/Modules/Analytics_4.php | 6 +++--- .../Analytics_4/Datapoints/Get_Has_Property_AccessTest.php | 2 -- .../Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php | 1 - .../Modules/Analytics_4/Datapoints/Get_ReportTest.php | 7 +++---- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/includes/Modules/Analytics_4.php b/includes/Modules/Analytics_4.php index 0ddf7e844e3..77fbc71d8ec 100644 --- a/includes/Modules/Analytics_4.php +++ b/includes/Modules/Analytics_4.php @@ -74,13 +74,13 @@ use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Container_Destinations; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Enhanced_Measurement_Settings; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Google_Tag_Settings; +use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Has_Property_Access; +use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Key_Events; +use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Report; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Webdatastreams; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Webdatastreams_Batch; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Audience_Settings; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Sync_Audiences; -use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Report; -use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Key_Events; -use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Has_Property_Access; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Custom_Dimension_Data_Available; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Save_Resource_Data_Availability_Date; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Set_Google_Tag_ID_Mismatch; diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php index 9090947327d..2eb6301b197 100644 --- a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Has_Property_AccessTest.php @@ -54,7 +54,6 @@ class Get_Has_Property_AccessTest extends TestCase { */ private $analytics; - public function set_up() { parent::set_up(); @@ -104,7 +103,6 @@ public function test_create_request__requires_property_id_param() { } } - public function test_create_request() { $this->get_report_request = null; diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php index bd47a22438e..e53f9b61296 100644 --- a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_Key_EventsTest.php @@ -12,7 +12,6 @@ use Google\Site_Kit\Core\REST_API\Data_Request; use Google\Site_Kit\Modules\Analytics_4\Datapoints\Get_Key_Events; -use Google\Site_Kit\Modules\Analytics_4\Report\Response as ReportResponse; use Google\Site_Kit\Tests\TestCase; use Google\Site_Kit\Context; use Google\Site_Kit\Core\Authentication\Authentication; diff --git a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php index daf69a4f6e6..3607e82886e 100644 --- a/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php +++ b/tests/phpunit/integration/Modules/Analytics_4/Datapoints/Get_ReportTest.php @@ -60,7 +60,7 @@ class Get_ReportTest extends TestCase { * * @var Analytics_4_Report_Response */ - private $reportResponse; + private $report_response; public function set_up() { parent::set_up(); @@ -75,14 +75,13 @@ public function set_up() { $this->analytics->get_client()->withDefer( true ); $service = new Google_Service_AnalyticsData( $this->analytics->get_client() ); - $this->reportResponse = new Analytics_4_Report_Response( $context ); + $this->report_response = new Analytics_4_Report_Response( $context ); $this->datapoint = new Get_Report( array( 'service' => function () use ( $service ) { return $service; }, - 'shareable' => true, 'settings' => $this->analytics->get_settings(), 'context' => $context, 'is_shared_request' => function () { @@ -168,7 +167,7 @@ public function test_parse_response() { $this->assertEquals( $response, - $this->reportResponse->parse_response( $data_request, new Google_Service_AnalyticsData_RunReportResponse() ), + $this->report_response->parse_response( $data_request, new Google_Service_AnalyticsData_RunReportResponse() ), 'The datapoint should parse the response using Analytics_4_Report_Response.' ); }