538 lines
17 KiB
PHP
538 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* Class Google\Site_Kit\Modules\Analytics_4\Report\SharedRequestHelpers
|
|
*
|
|
* @package Google\Site_Kit\Modules\Analytics_4\Report
|
|
* @copyright 2024 Google LLC
|
|
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
|
* @link https://sitekit.withgoogle.com
|
|
*/
|
|
|
|
namespace Google\Site_Kit\Modules\Analytics_4\Report;
|
|
|
|
use Google\Site_Kit\Context;
|
|
use Google\Site_Kit\Core\REST_API\Data_Request;
|
|
use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Dimensions_Exception;
|
|
use Google\Site_Kit\Core\Validation\Exception\Invalid_Report_Metrics_Exception;
|
|
use Google\Site_Kit\Core\Util\URL;
|
|
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Empty_Filter;
|
|
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\In_List_Filter;
|
|
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\String_Filter;
|
|
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Numeric_Filter;
|
|
use Google\Site_Kit\Modules\Analytics_4\Report\Filters\Between_Filter;
|
|
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Dimension as Google_Service_AnalyticsData_Dimension;
|
|
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpression as Google_Service_AnalyticsData_FilterExpression;
|
|
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\FilterExpressionList as Google_Service_AnalyticsData_FilterExpressionList;
|
|
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest;
|
|
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunPivotReportRequest as Google_Service_AnalyticsData_RunPivotReportRequest;
|
|
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Metric as Google_Service_AnalyticsData_Metric;
|
|
use WP_Error;
|
|
|
|
/**
|
|
* A class containing shared methods for creating AnalyticsData Report and PivotReport requests.
|
|
*
|
|
* @since 1.130.0
|
|
* @access private
|
|
* @ignore
|
|
*/
|
|
class RequestHelpers {
|
|
|
|
/**
|
|
* Plugin context.
|
|
*
|
|
* @since 1.130.0
|
|
* @var Context
|
|
*/
|
|
private $context;
|
|
|
|
/**
|
|
* Constructs a new instance of the class.
|
|
*
|
|
* @param Context $context Plugin context.
|
|
*/
|
|
public function __construct( $context ) {
|
|
$this->context = $context;
|
|
}
|
|
|
|
/**
|
|
* Builds a Analytics Data Report or Pivot Report request's shared properties.
|
|
*
|
|
* @since 1.130.0
|
|
*
|
|
* @param Data_Request $data Data request object.
|
|
* @param Google_Service_AnalyticsData_RunPivotReportRequest|Google_Service_AnalyticsData_RunReportRequest $request The report request object.
|
|
* @param bool $is_shared_request Determines whether the current request is shared or not.
|
|
* @return Google_Service_AnalyticsData_RunPivotReportRequest|Google_Service_AnalyticsData_RunReportRequest The report request object.
|
|
*/
|
|
public function shared_create_request( Data_Request $data, $request, $is_shared_request = false ) {
|
|
$keep_empty_rows = is_array( $data->data ) && array_key_exists( 'keepEmptyRows', $data->data ) ? filter_var( $data->data['keepEmptyRows'], FILTER_VALIDATE_BOOLEAN ) : true;
|
|
|
|
$request->setKeepEmptyRows( $keep_empty_rows );
|
|
|
|
$dimension_filters = $this->parse_dimension_filters( $data );
|
|
$request->setDimensionFilter( $dimension_filters );
|
|
|
|
$metric_filters = $this->parse_metric_filters( $data );
|
|
if ( ! empty( $metric_filters ) ) {
|
|
$request->setMetricFilter( $metric_filters );
|
|
}
|
|
|
|
$report_parsers = new ReportParsers();
|
|
$date_ranges = $report_parsers->parse_dateranges( $data );
|
|
$request->setDateRanges( $date_ranges );
|
|
|
|
$metrics = $data['metrics'];
|
|
if ( is_string( $metrics ) || is_array( $metrics ) ) {
|
|
if ( is_string( $metrics ) ) {
|
|
$metrics = explode( ',', $data['metrics'] );
|
|
} elseif ( is_array( $metrics ) && ! wp_is_numeric_array( $metrics ) ) { // If single object is passed.
|
|
$metrics = array( $metrics );
|
|
}
|
|
|
|
$metrics = array_filter(
|
|
array_map(
|
|
function ( $metric_def ) {
|
|
$metric = new Google_Service_AnalyticsData_Metric();
|
|
|
|
if ( is_string( $metric_def ) ) {
|
|
$metric->setName( $metric_def );
|
|
} elseif ( is_array( $metric_def ) ) {
|
|
$metric->setName( $metric_def['name'] );
|
|
if ( ! empty( $metric_def['expression'] ) ) {
|
|
$metric->setExpression( $metric_def['expression'] );
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
return $metric;
|
|
},
|
|
$metrics
|
|
)
|
|
);
|
|
|
|
if ( ! empty( $metrics ) ) {
|
|
try {
|
|
$this->validate_metrics( $metrics );
|
|
} catch ( Invalid_Report_Metrics_Exception $exception ) {
|
|
return new WP_Error(
|
|
'invalid_analytics_4_report_metrics',
|
|
$exception->getMessage()
|
|
);
|
|
}
|
|
|
|
if ( $is_shared_request ) {
|
|
try {
|
|
$this->validate_shared_metrics( $metrics );
|
|
} catch ( Invalid_Report_Metrics_Exception $exception ) {
|
|
return new WP_Error(
|
|
'invalid_analytics_4_report_metrics',
|
|
$exception->getMessage()
|
|
);
|
|
}
|
|
}
|
|
|
|
$request->setMetrics( $metrics );
|
|
}
|
|
}
|
|
|
|
return $request;
|
|
}
|
|
|
|
/**
|
|
* Validates the given metrics for a report.
|
|
*
|
|
* Metrics must have valid names, matching the regular expression ^[a-zA-Z0-9_]+$ in keeping with the GA4 API.
|
|
*
|
|
* @since 1.99.0
|
|
* @since 1.130.0 Moved into RequestHelpers for shared use between Report and PivotReport.
|
|
*
|
|
* @param Google_Service_AnalyticsData_Metric[] $metrics The metrics to validate.
|
|
* @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid.
|
|
*/
|
|
protected function validate_metrics( $metrics ) {
|
|
$valid_name_expression = '^[a-zA-Z0-9_]+$';
|
|
|
|
$invalid_metrics = array_map(
|
|
function ( $metric ) {
|
|
return $metric->getName();
|
|
},
|
|
array_filter(
|
|
$metrics,
|
|
function ( $metric ) use ( $valid_name_expression ) {
|
|
return ! preg_match( "#$valid_name_expression#", $metric->getName() ?? '' );
|
|
}
|
|
)
|
|
);
|
|
|
|
if ( count( $invalid_metrics ) > 0 ) {
|
|
$message = count( $invalid_metrics ) > 1 ? sprintf(
|
|
/* translators: 1: the regular expression for a valid name, 2: a comma separated list of the invalid metrics. */
|
|
__(
|
|
'Metric names should match the expression %1$s: %2$s',
|
|
'google-site-kit'
|
|
),
|
|
$valid_name_expression,
|
|
join(
|
|
/* translators: used between list items, there is a space after the comma. */
|
|
__( ', ', 'google-site-kit' ),
|
|
$invalid_metrics
|
|
)
|
|
) : sprintf(
|
|
/* translators: 1: the regular expression for a valid name, 2: the invalid metric. */
|
|
__(
|
|
'Metric name should match the expression %1$s: %2$s',
|
|
'google-site-kit'
|
|
),
|
|
$valid_name_expression,
|
|
$invalid_metrics[0]
|
|
);
|
|
|
|
throw new Invalid_Report_Metrics_Exception( $message );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates the report metrics for a shared request.
|
|
*
|
|
* @since 1.99.0
|
|
* @since 1.130.0 Moved into RequestHelpers for shared use between Report and PivotReport.
|
|
*
|
|
* @param Google_Service_AnalyticsData_Metric[] $metrics The metrics to validate.
|
|
* @throws Invalid_Report_Metrics_Exception Thrown if the metrics are invalid.
|
|
*/
|
|
protected function validate_shared_metrics( $metrics ) {
|
|
$valid_metrics = apply_filters(
|
|
'googlesitekit_shareable_analytics_4_metrics',
|
|
array(
|
|
'activeUsers',
|
|
'addToCarts',
|
|
'averageSessionDuration',
|
|
'bounceRate',
|
|
'conversions',
|
|
'ecommercePurchases',
|
|
'engagedSessions',
|
|
'engagementRate',
|
|
'eventCount',
|
|
'screenPageViews',
|
|
'screenPageViewsPerSession',
|
|
'sessions',
|
|
'sessionConversionRate',
|
|
'sessionsPerUser',
|
|
'totalAdRevenue',
|
|
'totalUsers',
|
|
)
|
|
);
|
|
|
|
$invalid_metrics = array_diff(
|
|
array_map(
|
|
function ( $metric ) {
|
|
// If there is an expression, it means the name is there as an alias, otherwise the name should be a valid metric name.
|
|
// Therefore, the expression takes precedence to the name for the purpose of allow-list validation.
|
|
return ! empty( $metric->getExpression() ) ? $metric->getExpression() : $metric->getName();
|
|
},
|
|
$metrics
|
|
),
|
|
$valid_metrics
|
|
);
|
|
|
|
if ( count( $invalid_metrics ) > 0 ) {
|
|
$message = count( $invalid_metrics ) > 1 ? sprintf(
|
|
/* translators: %s: is replaced with a comma separated list of the invalid metrics. */
|
|
__(
|
|
'Unsupported metrics requested: %s',
|
|
'google-site-kit'
|
|
),
|
|
join(
|
|
/* translators: used between list items, there is a space after the comma. */
|
|
__( ', ', 'google-site-kit' ),
|
|
$invalid_metrics
|
|
)
|
|
) : sprintf(
|
|
/* translators: %s: is replaced with the invalid metric. */
|
|
__(
|
|
'Unsupported metric requested: %s',
|
|
'google-site-kit'
|
|
),
|
|
$invalid_metrics[0]
|
|
);
|
|
|
|
throw new Invalid_Report_Metrics_Exception( $message );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates the report dimensions for a shared request.
|
|
*
|
|
* @since 1.99.0
|
|
* @since 1.130.0 Moved into RequestHelpers for shared use between Report and PivotReport.
|
|
*
|
|
* @param Google_Service_AnalyticsData_Dimension[] $dimensions The dimensions to validate.
|
|
* @throws Invalid_Report_Dimensions_Exception Thrown if the dimensions are invalid.
|
|
*/
|
|
public function validate_shared_dimensions( $dimensions ) {
|
|
$valid_dimensions = apply_filters(
|
|
'googlesitekit_shareable_analytics_4_dimensions',
|
|
array(
|
|
'audienceResourceName',
|
|
'adSourceName',
|
|
'city',
|
|
'country',
|
|
'date',
|
|
'deviceCategory',
|
|
'eventName',
|
|
'newVsReturning',
|
|
'pagePath',
|
|
'pageTitle',
|
|
'sessionDefaultChannelGroup',
|
|
'sessionDefaultChannelGrouping',
|
|
'customEvent:googlesitekit_post_author',
|
|
'customEvent:googlesitekit_post_categories',
|
|
'customEvent:googlesitekit_post_date',
|
|
'customEvent:googlesitekit_post_type',
|
|
)
|
|
);
|
|
|
|
$invalid_dimensions = array_diff(
|
|
array_map(
|
|
function ( $dimension ) {
|
|
return $dimension->getName();
|
|
},
|
|
$dimensions
|
|
),
|
|
$valid_dimensions
|
|
);
|
|
|
|
if ( count( $invalid_dimensions ) > 0 ) {
|
|
$message = count( $invalid_dimensions ) > 1 ? sprintf(
|
|
/* translators: %s: is replaced with a comma separated list of the invalid dimensions. */
|
|
__(
|
|
'Unsupported dimensions requested: %s',
|
|
'google-site-kit'
|
|
),
|
|
join(
|
|
/* translators: used between list items, there is a space after the comma. */
|
|
__( ', ', 'google-site-kit' ),
|
|
$invalid_dimensions
|
|
)
|
|
) : sprintf(
|
|
/* translators: %s: is replaced with the invalid dimension. */
|
|
__(
|
|
'Unsupported dimension requested: %s',
|
|
'google-site-kit'
|
|
),
|
|
$invalid_dimensions[0]
|
|
);
|
|
|
|
throw new Invalid_Report_Dimensions_Exception( $message );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses dimension filters and returns a filter expression that should be added to the report request.
|
|
*
|
|
* @since 1.106.0
|
|
* @since 1.130.0 Moved into RequestHelpers for shared use between Report and PivotReport.
|
|
*
|
|
* @param Data_Request $data Data request object.
|
|
* @return Google_Service_AnalyticsData_FilterExpression The filter expression to use with the report request.
|
|
*/
|
|
protected function parse_dimension_filters( Data_Request $data ) {
|
|
$expressions = array();
|
|
|
|
$reference_url = trim( $this->context->get_reference_site_url(), '/' );
|
|
$hostnames = URL::permute_site_hosts( URL::parse( $reference_url, PHP_URL_HOST ) );
|
|
$expressions[] = $this->parse_dimension_filter( 'hostName', $hostnames );
|
|
|
|
if ( ! empty( $data['url'] ) ) {
|
|
$url = str_replace( $reference_url, '', esc_url_raw( $data['url'] ) );
|
|
$expressions[] = $this->parse_dimension_filter( 'pagePath', $url );
|
|
}
|
|
|
|
if ( is_array( $data['dimensionFilters'] ) ) {
|
|
foreach ( $data['dimensionFilters'] as $key => $value ) {
|
|
$expressions[] = $this->parse_dimension_filter( $key, $value );
|
|
}
|
|
}
|
|
|
|
$filter_expression_list = new Google_Service_AnalyticsData_FilterExpressionList();
|
|
$filter_expression_list->setExpressions( array_filter( $expressions ) );
|
|
|
|
$dimension_filters = new Google_Service_AnalyticsData_FilterExpression();
|
|
$dimension_filters->setAndGroup( $filter_expression_list );
|
|
|
|
return $dimension_filters;
|
|
}
|
|
|
|
/**
|
|
* Parses and returns a single dimension filter.
|
|
*
|
|
* @since 1.106.0
|
|
* @since 1.130.0 Moved into RequestHelpers for shared use between Report and PivotReport.
|
|
*
|
|
* @param string $dimension_name The dimension name.
|
|
* @param mixed $dimension_value The dimension fileter settings.
|
|
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
|
|
*/
|
|
protected function parse_dimension_filter( $dimension_name, $dimension_value ) {
|
|
// Use the string filter type by default.
|
|
$filter_type = 'stringFilter';
|
|
if ( isset( $dimension_value['filterType'] ) ) {
|
|
// If the filterType property is provided, use the explicit filter type then.
|
|
$filter_type = $dimension_value['filterType'];
|
|
} elseif ( wp_is_numeric_array( $dimension_value ) ) {
|
|
// Otherwise, if the dimension has a numeric array of values, we should fall
|
|
// back to the "in list" filter type.
|
|
$filter_type = 'inListFilter';
|
|
}
|
|
|
|
if ( 'stringFilter' === $filter_type ) {
|
|
$filter_class = String_Filter::class;
|
|
} elseif ( 'inListFilter' === $filter_type ) {
|
|
$filter_class = In_List_Filter::class;
|
|
// Ensure that the 'inListFilter' is provided a flat array of values.
|
|
// Extract the actual values from the 'value' key if present.
|
|
if ( isset( $dimension_value['value'] ) ) {
|
|
$dimension_value = $dimension_value['value'];
|
|
}
|
|
} elseif ( 'emptyFilter' === $filter_type ) {
|
|
$filter_class = Empty_Filter::class;
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
$filter = new $filter_class();
|
|
$filter_expression = $filter->parse_filter_expression( $dimension_name, $dimension_value );
|
|
|
|
if ( ! empty( $dimension_value['notExpression'] ) ) {
|
|
$not_filter_expression = new Google_Service_AnalyticsData_FilterExpression();
|
|
$not_filter_expression->setNotExpression( $filter_expression );
|
|
return $not_filter_expression;
|
|
}
|
|
|
|
return $filter_expression;
|
|
}
|
|
|
|
/**
|
|
* Parses metric filters and returns a filter expression that should be added to the report request.
|
|
*
|
|
* @since 1.111.0
|
|
* @since 1.130.0 Moved into RequestHelpers for shared use between Report and PivotReport.
|
|
*
|
|
* @param Data_Request $data Data request object.
|
|
* @return Google_Service_AnalyticsData_FilterExpression The filter expression to use with the report request.
|
|
*/
|
|
protected function parse_metric_filters( Data_Request $data ) {
|
|
$expressions = array();
|
|
|
|
if ( is_array( $data['metricFilters'] ) ) {
|
|
foreach ( $data['metricFilters'] as $key => $value ) {
|
|
$expressions[] = $this->parse_metric_filter( $key, $value );
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $expressions ) ) {
|
|
$filter_expression_list = new Google_Service_AnalyticsData_FilterExpressionList();
|
|
$filter_expression_list->setExpressions( array_filter( $expressions ) );
|
|
|
|
$metric_filters = new Google_Service_AnalyticsData_FilterExpression();
|
|
$metric_filters->setAndGroup( $filter_expression_list );
|
|
|
|
return $metric_filters;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Parses and returns a single metric filter.
|
|
*
|
|
* @since 1.111.0
|
|
* @since 1.130.0 Moved into RequestHelpers for shared use between Report and PivotReport.
|
|
*
|
|
* @param string $metric_name The metric name.
|
|
* @param mixed $metric_value The metric filter settings.
|
|
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
|
|
*/
|
|
protected function parse_metric_filter( $metric_name, $metric_value ) {
|
|
// Use the numeric filter type by default.
|
|
$filter_type = 'numericFilter';
|
|
if ( isset( $metric_value['filterType'] ) ) {
|
|
// If the filterType property is provided, use the explicit filter type then.
|
|
$filter_type = $metric_value['filterType'];
|
|
}
|
|
|
|
if ( 'numericFilter' === $filter_type ) {
|
|
if ( ! isset( $metric_value['operation'] ) || ! isset( $metric_value['value'] ) ) {
|
|
return null;
|
|
}
|
|
if ( ! isset( $metric_value['value']['int64Value'] ) ) {
|
|
return null;
|
|
}
|
|
|
|
$filter = new Numeric_Filter();
|
|
|
|
} elseif ( 'betweenFilter' === $filter_type ) {
|
|
if ( ! isset( $metric_value['from_value'] ) || ! isset( $metric_value['to_value'] ) ) {
|
|
return null;
|
|
}
|
|
if (
|
|
! isset( $metric_value['from_value']['int64Value'] ) ||
|
|
! isset( $metric_value['to_value']['int64Value'] )
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
$filter = new Between_Filter();
|
|
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
$filter_expression = $this->get_metric_filter_expression(
|
|
$filter,
|
|
$metric_name,
|
|
$metric_value
|
|
);
|
|
|
|
return $filter_expression;
|
|
}
|
|
|
|
/**
|
|
* Returns correct filter expression instance based on the metric filter instance.
|
|
*
|
|
* @since 1.111.0
|
|
* @since 1.130.0 Moved into RequestHelpers for shared use between Report and PivotReport.
|
|
*
|
|
* @param Numeric_Filter|Between_Filter $filter The metric filter instance.
|
|
* @param string $metric_name The metric name.
|
|
* @param mixed $metric_value The metric filter settings.
|
|
* @return Google_Service_AnalyticsData_FilterExpression The filter expression instance.
|
|
*/
|
|
protected function get_metric_filter_expression( $filter, $metric_name, $metric_value ) {
|
|
if ( $filter instanceof Numeric_Filter ) {
|
|
$value = $metric_value['value']['int64Value'];
|
|
|
|
$filter_expression = $filter->parse_filter_expression(
|
|
$metric_name,
|
|
$metric_value['operation'],
|
|
$value
|
|
);
|
|
|
|
} elseif ( $filter instanceof Between_Filter ) {
|
|
$from_value = $metric_value['from_value']['int64Value'];
|
|
$to_value = $metric_value['to_value']['int64Value'];
|
|
|
|
$filter_expression = $filter->parse_filter_expression(
|
|
$metric_name,
|
|
$from_value,
|
|
$to_value
|
|
);
|
|
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
return $filter_expression;
|
|
}
|
|
}
|