PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.5
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / dashboard / infrastructure / analytics-4 / site-kit-analytics-4-adapter.php

site-kit-analytics-4-adapter.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.5, at src/dashboard/infrastructure/analytics-4/site-kit-analytics-4-adapter.php

283 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
4 // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
5 namespace Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4;
6
7 use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Row;
8 use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse;
9 use WP_REST_Response;
10 use Yoast\WP\SEO\Dashboard\Domain\Analytics_4\Failed_Request_Exception;
11 use Yoast\WP\SEO\Dashboard\Domain\Analytics_4\Invalid_Request_Exception;
12 use Yoast\WP\SEO\Dashboard\Domain\Analytics_4\Unexpected_Response_Exception;
13 use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
14 use Yoast\WP\SEO\Dashboard\Domain\Traffic\Comparison_Traffic_Data;
15 use Yoast\WP\SEO\Dashboard\Domain\Traffic\Daily_Traffic_Data;
16 use Yoast\WP\SEO\Dashboard\Domain\Traffic\Traffic_Data;
17
18 /**
19 * The site API adapter to make calls to the Analytics 4 API, via the Site_Kit plugin.
20 */
21 class Site_Kit_Analytics_4_Adapter {
22
23 /**
24 * Holds the api call class.
25 *
26 * @var Site_Kit_Analytics_4_Api_Call $site_kit_analytics_4_api_call
27 */
28 private $site_kit_search_console_api_call;
29
30 /**
31 * The register method that sets the instance in the adapter.
32 *
33 * @param Site_Kit_Analytics_4_Api_Call $site_kit_analytics_4_api_call The api call class.
34 *
35 * @return void
36 */
37 public function __construct( Site_Kit_Analytics_4_Api_Call $site_kit_analytics_4_api_call ) {
38 $this->site_kit_search_console_api_call = $site_kit_analytics_4_api_call;
39 }
40
41 /**
42 * The wrapper method to do a comparison Site Kit API request for Analytics.
43 *
44 * @param Analytics_4_Parameters $parameters The parameters.
45 *
46 * @return Data_Container The Site Kit API response.
47 *
48 * @throws Failed_Request_Exception When the request responds with an error from Site Kit.
49 * @throws Unexpected_Response_Exception When the request responds with an unexpected format.
50 * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
51 */
52 public function get_comparison_data( Analytics_4_Parameters $parameters ): Data_Container {
53 $api_parameters = $this->build_parameters( $parameters );
54
55 $response = $this->site_kit_search_console_api_call->do_request( $api_parameters );
56
57 $this->validate_response( $response );
58
59 return $this->parse_comparison_response( $response->get_data() );
60 }
61
62 /**
63 * The wrapper method to do a daily Site Kit API request for Analytics.
64 *
65 * @param Analytics_4_Parameters $parameters The parameters.
66 *
67 * @return Data_Container The Site Kit API response.
68 *
69 * @throws Failed_Request_Exception When the request responds with an error from Site Kit.
70 * @throws Unexpected_Response_Exception When the request responds with an unexpected format.
71 * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
72 */
73 public function get_daily_data( Analytics_4_Parameters $parameters ): Data_Container {
74 $api_parameters = $this->build_parameters( $parameters );
75
76 $response = $this->site_kit_search_console_api_call->do_request( $api_parameters );
77
78 $this->validate_response( $response );
79
80 return $this->parse_daily_response( $response->get_data() );
81 }
82
83 /**
84 * Builds the parameters to be used in the Site Kit API request.
85 *
86 * @param Analytics_4_Parameters $parameters The parameters.
87 *
88 * @return array<string, array<string, string>> The Site Kit API parameters.
89 */
90 private function build_parameters( Analytics_4_Parameters $parameters ): array {
91 $api_parameters = [
92 'slug' => 'analytics-4',
93 'datapoint' => 'report',
94 'startDate' => $parameters->get_start_date(),
95 'endDate' => $parameters->get_end_date(),
96 ];
97
98 if ( ! empty( $parameters->get_dimension_filters() ) ) {
99 $api_parameters['dimensionFilters'] = $parameters->get_dimension_filters();
100 }
101
102 if ( ! empty( $parameters->get_dimensions() ) ) {
103 $api_parameters['dimensions'] = $parameters->get_dimensions();
104 }
105
106 if ( ! empty( $parameters->get_metrics() ) ) {
107 $api_parameters['metrics'] = $parameters->get_metrics();
108 }
109
110 if ( ! empty( $parameters->get_order_by() ) ) {
111 $api_parameters['orderby'] = $parameters->get_order_by();
112 }
113
114 if ( ! empty( $parameters->get_compare_start_date() && ! empty( $parameters->get_compare_end_date() ) ) ) {
115 $api_parameters['compareStartDate'] = $parameters->get_compare_start_date();
116 $api_parameters['compareEndDate'] = $parameters->get_compare_end_date();
117 }
118
119 return $api_parameters;
120 }
121
122 /**
123 * Parses a response for a Site Kit API request that requests daily data for Analytics 4.
124 *
125 * @param RunReportResponse $response The response to parse.
126 *
127 * @return Data_Container The parsed response.
128 *
129 * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
130 */
131 private function parse_daily_response( RunReportResponse $response ): Data_Container {
132 if ( ! $this->is_daily_request( $response ) ) {
133 throw new Invalid_Request_Exception( 'Unexpected parameters for the request' );
134 }
135
136 $data_container = new Data_Container();
137
138 foreach ( $response->getRows() as $daily_traffic ) {
139 $traffic_data = new Traffic_Data();
140
141 foreach ( $response->getMetricHeaders() as $key => $metric ) {
142
143 // As per https://developers.google.com/analytics/devguides/reporting/data/v1/basics#read_the_response,
144 // the order of the columns is consistent in the request, header, and rows.
145 // So we can use the key of the header to get the correct metric value from the row.
146 $metric_value = $daily_traffic->getMetricValues()[ $key ]->getValue();
147
148 if ( $metric->getName() === 'sessions' ) {
149 $traffic_data->set_sessions( (int) $metric_value );
150 }
151 elseif ( $metric->getName() === 'totalUsers' ) {
152 $traffic_data->set_total_users( (int) $metric_value );
153 }
154 }
155
156 // Since we're here, we know that the first dimension is date, so we know that dimensionValues[0]->value is a date.
157 $data_container->add_data( new Daily_Traffic_Data( $daily_traffic->getDimensionValues()[0]->getValue(), $traffic_data ) );
158 }
159
160 return $data_container;
161 }
162
163 /**
164 * Parses a response for a Site Kit API request for Analytics 4 that compares data ranges.
165 *
166 * @param RunReportResponse $response The response to parse.
167 *
168 * @return Data_Container The parsed response.
169 *
170 * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
171 */
172 private function parse_comparison_response( RunReportResponse $response ): Data_Container {
173 if ( ! $this->is_comparison_request( $response ) ) {
174 throw new Invalid_Request_Exception( 'Unexpected parameters for the request' );
175 }
176
177 $data_container = new Data_Container();
178 $comparison_traffic_data = new Comparison_Traffic_Data();
179
180 // First row is the current date range's data, second row is the previous date range's data.
181 foreach ( $response->getRows() as $date_range_row ) {
182 $traffic_data = new Traffic_Data();
183
184 // Loop through all the metrics of the date range.
185 foreach ( $response->getMetricHeaders() as $key => $metric ) {
186
187 // As per https://developers.google.com/analytics/devguides/reporting/data/v1/basics#read_the_response,
188 // the order of the columns is consistent in the request, header, and rows.
189 // So we can use the key of the header to get the correct metric value from the row.
190 $metric_value = $date_range_row->getMetricValues()[ $key ]->getValue();
191
192 if ( $metric->getName() === 'sessions' ) {
193 $traffic_data->set_sessions( (int) $metric_value );
194 }
195 elseif ( $metric->getName() === 'totalUsers' ) {
196 $traffic_data->set_total_users( (int) $metric_value );
197 }
198 }
199
200 $period = $this->get_period( $date_range_row );
201
202 if ( $period === Comparison_Traffic_Data::CURRENT_PERIOD_KEY ) {
203 $comparison_traffic_data->set_current_traffic_data( $traffic_data );
204 }
205 elseif ( $period === Comparison_Traffic_Data::PREVIOUS_PERIOD_KEY ) {
206 $comparison_traffic_data->set_previous_traffic_data( $traffic_data );
207 }
208 }
209
210 $data_container->add_data( $comparison_traffic_data );
211
212 return $data_container;
213 }
214
215 /**
216 * Parses the response row and returns whether it's about the current period or the previous period.
217 *
218 * @see https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/DateRange
219 *
220 * @param Row $date_range_row The response row.
221 *
222 * @return string The key associated with the current or the previous period.
223 *
224 * @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
225 */
226 private function get_period( Row $date_range_row ): string {
227 foreach ( $date_range_row->getDimensionValues() as $dimension_value ) {
228 if ( $dimension_value->getValue() === 'date_range_0' ) {
229 return Comparison_Traffic_Data::CURRENT_PERIOD_KEY;
230 }
231 elseif ( $dimension_value->getValue() === 'date_range_1' ) {
232 return Comparison_Traffic_Data::PREVIOUS_PERIOD_KEY;
233 }
234 }
235
236 throw new Invalid_Request_Exception( 'Unexpected date range names' );
237 }
238
239 /**
240 * Checks the response of the request to detect if it's a comparison request.
241 *
242 * @param RunReportResponse $response The response.
243 *
244 * @return bool Whether it's a comparison request.
245 */
246 private function is_comparison_request( RunReportResponse $response ): bool {
247 return \count( $response->getDimensionHeaders() ) === 1 && $response->getDimensionHeaders()[0]->getName() === 'dateRange';
248 }
249
250 /**
251 * Checks the response of the request to detect if it's a daily request.
252 *
253 * @param RunReportResponse $response The response.
254 *
255 * @return bool Whether it's a daily request.
256 */
257 private function is_daily_request( RunReportResponse $response ): bool {
258 return \count( $response->getDimensionHeaders() ) === 1 && $response->getDimensionHeaders()[0]->getName() === 'date';
259 }
260
261 /**
262 * Validates the response coming from Google Analytics.
263 *
264 * @param WP_REST_Response $response The response we want to validate.
265 *
266 * @return void
267 *
268 * @throws Failed_Request_Exception When the request responds with an error from Site Kit.
269 * @throws Unexpected_Response_Exception When the request responds with an unexpected format.
270 */
271 private function validate_response( WP_REST_Response $response ): void {
272 if ( $response->is_error() ) {
273 $error_data = $response->as_error()->get_error_data();
274 $error_status_code = ( $error_data['status'] ?? 500 );
275 throw new Failed_Request_Exception( \wp_kses_post( $response->as_error()->get_error_message() ), (int) $error_status_code );
276 }
277
278 if ( ! \is_a( $response->get_data(), RunReportResponse::class ) ) {
279 throw new Unexpected_Response_Exception();
280 }
281 }
282 }
283