Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement/6623 ga4 widgets real data #6790

Merged
merged 31 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f18e950
Add the new parse_report_response method.
eugene-manuilov Mar 28, 2023
e349669
Add the parse_reporting_dimensions method.
eugene-manuilov Mar 28, 2023
0bed654
Add the parse_reporting_dateranges method.
eugene-manuilov Mar 28, 2023
797578c
Implement the parse_report_response method.
eugene-manuilov Mar 28, 2023
4c06396
Update rows count.
eugene-manuilov Mar 28, 2023
6ca7774
Merge remote-tracking branch 'origin/develop' into enhancement/6623-g…
eugene-manuilov Mar 29, 2023
680a723
Update the parse_report_response method to set default values to all …
eugene-manuilov Mar 29, 2023
2a2d97e
Fix formatting issues.
eugene-manuilov Mar 29, 2023
97e835a
Add tests for the new method.
eugene-manuilov Mar 29, 2023
e130cc9
Apply suggestions from code review
eugene-manuilov Mar 30, 2023
eb5e884
Fix typos.
eugene-manuilov Mar 30, 2023
4aa8fec
Addressed code review feedback.
eugene-manuilov Mar 30, 2023
182b2fe
Moved the report related functionality to its own class.
eugene-manuilov Mar 30, 2023
3b5a4c8
Fix formatting issues.
eugene-manuilov Mar 30, 2023
d71278a
Update the test_parse_response method to use expected dates array.
eugene-manuilov Mar 30, 2023
fd91b5a
Fixed ordering issues.
eugene-manuilov Mar 31, 2023
b458c4f
Merge remote-tracking branch 'origin/develop' into enhancement/6623-g…
eugene-manuilov Mar 31, 2023
1dc3c19
Refactor the Report class into Response and Request ones.
eugene-manuilov Mar 31, 2023
84cb586
Add the Row_Trait trait.
eugene-manuilov Mar 31, 2023
73384fa
Update the default value for all metrics to be '0'.
eugene-manuilov Mar 31, 2023
43f6d6e
Fix formatting issues.
eugene-manuilov Mar 31, 2023
ebe895f
Merge remote-tracking branch 'origin/develop' into enhancement/6623-g…
eugene-manuilov Apr 3, 2023
78387da
Fix date ranges issue and add more tests.
eugene-manuilov Apr 3, 2023
fb98422
Address code review feedback.
eugene-manuilov Apr 3, 2023
53f76e1
Fix typo.
eugene-manuilov Apr 3, 2023
1557f86
Add $existing_rows to use clause.
techanvil Apr 3, 2023
c2ec3cf
Fix typo in comment.
techanvil Apr 3, 2023
9ae6e2b
Update tests to check initial data as well.
eugene-manuilov Apr 4, 2023
da96ecf
Fix formatting issues.
eugene-manuilov Apr 4, 2023
054afa9
Merge remote-tracking branch 'origin/develop' into enhancement/6623-g…
eugene-manuilov Apr 4, 2023
ac2339a
Simplify test and make expectations more verbose.
techanvil Apr 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 203 additions & 70 deletions includes/Modules/Analytics_4.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,17 @@
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\DimensionOrderBy as Google_Service_AnalyticsData_DimensionOrderBy;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\DimensionValue as Google_Service_AnalyticsData_DimensionValue;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Filter as Google_Service_AnalyticsData_Filter;
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\InListFilter as Google_Service_AnalyticsData_InListFilter;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricOrderBy as Google_Service_AnalyticsData_MetricOrderBy;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricValue as Google_Service_AnalyticsData_MetricValue;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\OrderBy as Google_Service_AnalyticsData_OrderBy;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Row as Google_Service_AnalyticsData_Row;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportRequest as Google_Service_AnalyticsData_RunReportRequest;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse as Google_Service_AnalyticsData_RunReportResponse;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\StringFilter as Google_Service_AnalyticsData_StringFilter;
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;
Expand Down Expand Up @@ -723,6 +727,8 @@ function( $property ) {
return $this->get_google_tag_settings_for_measurement_id( $response, $data['measurementID'] );
case 'GET:conversion-events':
return (array) $response->getConversionEvents();
case 'GET:report':
return $this->parse_report_response( $data, $response );
}

return parent::parse_data_response( $data, $response );
Expand Down Expand Up @@ -759,7 +765,6 @@ protected function setup_info() {
);
}


/**
* Gets the configured Analytics Data service object instance.
*
Expand Down Expand Up @@ -1150,47 +1155,20 @@ protected function create_report_request( Data_Request $data ) {
$request_args['row_limit'] = $data['limit'];
}

$dimensions = $data['dimensions'];
if ( ! empty( $dimensions ) && ( is_string( $dimensions ) || is_array( $dimensions ) ) ) {
if ( is_string( $dimensions ) ) {
$dimensions = explode( ',', $dimensions );
} elseif ( is_array( $dimensions ) && ! wp_is_numeric_array( $dimensions ) ) { // If single object is passed.
$dimensions = array( $dimensions );
}

$dimensions = array_filter(
array_map(
function ( $dimension_def ) {
$dimension = new Google_Service_AnalyticsData_Dimension();

if ( is_string( $dimension_def ) ) {
$dimension->setName( $dimension_def );
} elseif ( is_array( $dimension_def ) && ! empty( $dimension_def['name'] ) ) {
$dimension->setName( $dimension_def['name'] );
} else {
return null;
}

return $dimension;
},
array_filter( $dimensions )
)
);

if ( ! empty( $dimensions ) ) {
if ( $this->is_shared_data_request( $data ) ) {
try {
$this->validate_shared_report_dimensions( $dimensions );
} catch ( Invalid_Report_Dimensions_Exception $exception ) {
return new WP_Error(
'invalid_analytics_4_report_dimensions',
$exception->getMessage()
);
}
$dimensions = $this->parse_reporting_dimensions( $data );
if ( ! empty( $dimensions ) ) {
if ( $this->is_shared_data_request( $data ) ) {
try {
$this->validate_shared_report_dimensions( $dimensions );
} catch ( Invalid_Report_Dimensions_Exception $exception ) {
return new WP_Error(
'invalid_analytics_4_report_dimensions',
$exception->getMessage()
);
}

$request_args['dimensions'] = $dimensions;
}

$request_args['dimensions'] = $dimensions;
}

$dimension_filters = $data['dimensionFilters'];
Expand Down Expand Up @@ -1225,36 +1203,7 @@ function ( $dimension_def ) {
return $request;
}

$date_ranges = array();
$start_date = $data['startDate'];
$end_date = $data['endDate'];
if ( strtotime( $start_date ) && strtotime( $end_date ) ) {
$compare_start_date = $data['compareStartDate'];
$compare_end_date = $data['compareEndDate'];
$date_ranges[] = array( $start_date, $end_date );

// When using multiple date ranges, it changes the structure of the response:
// Aggregate properties (minimum, maximum, totals) will have an entry per date range.
// The rows property will have additional row entries for each date range.
if ( strtotime( $compare_start_date ) && strtotime( $compare_end_date ) ) {
$date_ranges[] = array( $compare_start_date, $compare_end_date );
}
} else {
// Default the date range to the last 28 days.
$date_ranges[] = $this->parse_date_range( 'last-28-days', 1 );
}

$date_ranges = array_map(
function ( $date_range ) {
list ( $start_date, $end_date ) = $date_range;
$date_range = new Google_Service_AnalyticsData_DateRange();
$date_range->setStartDate( $start_date );
$date_range->setEndDate( $end_date );

return $date_range;
},
$date_ranges
);
$date_ranges = $this->parse_reporting_dateranges( $data );
$request->setDateRanges( $date_ranges );

$metrics = $data['metrics'];
Expand Down Expand Up @@ -1330,6 +1279,91 @@ function ( $metric_def ) {
return $this->get_analyticsdata_service()->properties->runReport( self::normalize_property_id( $option['propertyID'] ), $request );
}

/**
* Parses report date ranges received in the request params.
*
* @since n.e.x.t
*
* @param Data_Request $data Data request object.
* @return array An array of parsed date ranges.
eugene-manuilov marked this conversation as resolved.
Show resolved Hide resolved
*/
protected function parse_reporting_dateranges( Data_Request $data ) {
$date_ranges = array();
$start_date = $data['startDate'];
$end_date = $data['endDate'];
if ( strtotime( $start_date ) && strtotime( $end_date ) ) {
$compare_start_date = $data['compareStartDate'];
$compare_end_date = $data['compareEndDate'];
$date_ranges[] = array( $start_date, $end_date );

// When using multiple date ranges, it changes the structure of the response:
// Aggregate properties (minimum, maximum, totals) will have an entry per date range.
// The rows property will have additional row entries for each date range.
if ( strtotime( $compare_start_date ) && strtotime( $compare_end_date ) ) {
$date_ranges[] = array( $compare_start_date, $compare_end_date );
}
} else {
// Default the date range to the last 28 days.
$date_ranges[] = $this->parse_date_range( 'last-28-days', 1 );
}

$date_ranges = array_map(
function ( $date_range ) {
list ( $start_date, $end_date ) = $date_range;
$date_range = new Google_Service_AnalyticsData_DateRange();
$date_range->setStartDate( $start_date );
$date_range->setEndDate( $end_date );

return $date_range;
},
$date_ranges
);

return $date_ranges;
}

/**
* Parses report dimensions received in the request params.
*
* @since n.e.x.t
*
* @param Data_Request $data Data request object.
* @return array An array of parsed dimensions.
eugene-manuilov marked this conversation as resolved.
Show resolved Hide resolved
*/
protected function parse_reporting_dimensions( Data_Request $data ) {
$dimensions = $data['dimensions'];
if ( empty( $dimensions ) || ( ! is_string( $dimensions ) && ! is_array( $dimensions ) ) ) {
return array();
}

if ( is_string( $dimensions ) ) {
$dimensions = explode( ',', $dimensions );
} elseif ( is_array( $dimensions ) && ! wp_is_numeric_array( $dimensions ) ) { // If single object is passed.
$dimensions = array( $dimensions );
}

$dimensions = array_filter(
array_map(
function ( $dimension_def ) {
$dimension = new Google_Service_AnalyticsData_Dimension();

if ( is_string( $dimension_def ) ) {
$dimension->setName( $dimension_def );
} elseif ( is_array( $dimension_def ) && ! empty( $dimension_def['name'] ) ) {
$dimension->setName( $dimension_def['name'] );
} else {
return null;
}

return $dimension;
},
array_filter( $dimensions )
)
);

return $dimensions;
}

/**
* Parses the orderby value of the data request into an array of AnalyticsData OrderBy object instances.
*
Expand Down Expand Up @@ -1372,6 +1406,104 @@ function ( $order_def ) {
return $results;
}

/**
* Parses the report response.
eugene-manuilov marked this conversation as resolved.
Show resolved Hide resolved
*
* @since n.e.x.t
*
* @param Data_Request $data Data request object.
* @param Google_Service_AnalyticsData_RunReportResponse $response Request response.
* @return mixed Parsed response data on success, or WP_Error on failure.
*/
protected function parse_report_response( Data_Request $data, $response ) {
// Return early if the response is not of the expected type.
if ( ! $response instanceof Google_Service_AnalyticsData_RunReportResponse ) {
return $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_reporting_dimensions( $data );
if ( count( $dimensions ) !== 1 || $dimensions[0]->getName() !== 'date' ) {
return $response;
}

// Get date ranges and return early if there is no date ranges for this report.
eugene-manuilov marked this conversation as resolved.
Show resolved Hide resolved
$date_ranges = $this->parse_reporting_dateranges( $data );
if ( empty( $date_ranges ) ) {
return $response;
}

$rows = $response->getRows();
$metrics = $response->getMetricHeaders();

foreach ( $date_ranges as $date_range ) {
$start = strtotime( $date_range->getStartDate() );
$end = strtotime( $date_range->getEndDate() );

// Skip this date range if either start date or end date is corrupted.
if ( ! $start || ! $end ) {
continue;
}

// Loop through all days in the date range and check if there is a metric value
// for it. If the metric value is missing, we will need to add one with a zero value.
$now = $start;
do {
$current_date = gmdate( 'Ymd', $now );

// Search for the current date in the response rows.
$found_dimenension = false;
eugene-manuilov marked this conversation as resolved.
Show resolved Hide resolved
foreach ( $rows as $row ) {
techanvil marked this conversation as resolved.
Show resolved Hide resolved
$dimention_values = $row->getDimensionValues();
eugene-manuilov marked this conversation as resolved.
Show resolved Hide resolved
if ( $dimention_values[0]->getValue() === $current_date ) {
$found_dimenension = true;
break;
}
}

// If the current date is not found, add the new row to the existing rows.
if ( ! $found_dimenension ) {
$dimension_value = new Google_Service_AnalyticsData_DimensionValue();
$dimension_value->setValue( $current_date );

$metric_values = array();
foreach ( $metrics as $metric ) {
$metric_value = new Google_Service_AnalyticsData_MetricValue();

switch ( $metric->getType() ) {
case 'TYPE_INTEGER':
case 'TYPE_FLOAT':
case 'TYPE_CURRENCY':
$metric_value->setValue( 0 );
techanvil marked this conversation as resolved.
Show resolved Hide resolved
break;
default:
$metric_value->setValue( null );
break;
}

$metric_values[] = $metric_value;
}

$row = new Google_Service_AnalyticsData_Row();
$row->setDimensionValues( array( $dimension_value ) );
$row->setMetricValues( $metric_values );

$rows[] = $row;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here needs to be updated based on how many date ranges there are. This is correct for the case where there is a single date range, but when there are multiple date ranges, we need to add a row for each date range, with a 2nd dimension value which is date_range_X, where X is the zero-based index of the date range.

You can see this in action by requesting a multi-date-range report. For illustration here's one of our fixtures which shows the same thing:

"rows": [
{
"dimensionValues": [
{ "value": "20201201" },
{ "value": "date_range_0" }
],
"metricValues": [ { "value": "55" }, { "value": "8" } ]
},
{
"dimensionValues": [
{ "value": "20201201" },
{ "value": "date_range_1" }
],
"metricValues": [ { "value": "3" }, { "value": "8" } ]
},
{
"dimensionValues": [
{ "value": "20201202" },
{ "value": "date_range_0" }
],
"metricValues": [ { "value": "17" }, { "value": "23" } ]
},
{
"dimensionValues": [
{ "value": "20201202" },
{ "value": "date_range_1" }
],
"metricValues": [ { "value": "10" }, { "value": "44" } ]
},

Furthermore - the logic also needs changing to ensure the rows are inserted in the right order, when date is provided as an order-by dimension. As GA4 reports no longer return rows ordered by default, we now need to explicitly specify for them to be ordered by date for our charts. As things stand this code simply appends the dates to the end of $rows which can result in an out of order sequence when there are some rows returned in the response.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, added the date range index and updated the logic to ensure that rows are sorted correctly.

Copy link
Collaborator

@techanvil techanvil Mar 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, as mentioned on a separate comment though, the logic still needs a bit of an update. Details are on the other comment.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update, but this still needs another tweak, with details on a separate comment.

}

// Add a day in seconds value to the current date to shift to the next date.
$now += DAY_IN_SECONDS;
} while ( $now <= $end );
}

// Set updated rows back to the response object.
$response->setRows( $rows );
$response->setRowCount( count( $rows ) );

return $response;
}

/**
* Creates a new Analytics 4 site request for the current site and given arguments.
*
Expand Down Expand Up @@ -1619,4 +1751,5 @@ function ( $dimension ) {
throw new Invalid_Report_Dimensions_Exception( $message );
}
}

}
41 changes: 41 additions & 0 deletions tests/phpunit/integration/Modules/Analytics_4Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Google\Site_Kit\Core\Modules\Module_With_Service_Entity;
use Google\Site_Kit\Core\Modules\Modules;
use Google\Site_Kit\Core\Permissions\Permissions;
use Google\Site_Kit\Core\REST_API\Data_Request;
use Google\Site_Kit\Core\Storage\Options;
use Google\Site_Kit\Core\Storage\User_Options;
use Google\Site_Kit\Modules\Analytics;
Expand All @@ -37,6 +38,8 @@
use Google\Site_Kit\Tests\TestCase;
use Google\Site_Kit\Tests\UserAuthenticationTrait;
use Google\Site_Kit_Dependencies\Google\Service\Exception;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\MetricHeader;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaConversionEvent;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStream;
use Google\Site_Kit_Dependencies\Google\Service\GoogleAnalyticsAdmin\GoogleAnalyticsAdminV1betaDataStreamWebStreamData;
Expand Down Expand Up @@ -1142,6 +1145,44 @@ public function test_get_report__dimensions_as_single_object( $access_token ) {
);
}

public function test_get_report__missing_rows() {
$report_args = array(
'startDate' => '2023-02-01',
'endDate' => '2023-02-03',
'compareStartDate' => '2023-01-01',
'compareEndDate' => '2023-01-03',
'dimensions' => 'date',
);

$first_metric = new MetricHeader();
$first_metric->setType( 'TYPE_INTEGER' );

$second_metric = new MetricHeader();
$second_metric->setType( 'TYPE_KILOMETERS' );

$data = new Data_Request( '', '', '', '', $report_args );
$response = new RunReportResponse();
$response->setMetricHeaders( array( $first_metric, $second_metric ) );

$reflected_parse_report_response_method = new \ReflectionMethod( Analytics_4::class, 'parse_report_response' );
techanvil marked this conversation as resolved.
Show resolved Hide resolved
$reflected_parse_report_response_method->setAccessible( true );
$response = $reflected_parse_report_response_method->invoke( $this->analytics, $data, $response );

$this->assertEquals( 6, $response->getRowCount() );

foreach ( $response->getRows() as $i => $row ) {
$date = strtotime( $i < 3 ? '2023-02-01' : '2023-01-01' ) + ( $i % 3 ) * DAY_IN_SECONDS;
techanvil marked this conversation as resolved.
Show resolved Hide resolved
$date = gmdate( 'Ymd', $date );

$dimension_values = $row->getDimensionValues();
$this->assertEquals( $date, $dimension_values[0]->getValue() );

$metric_values = $row->getMetricValues();
$this->assertEquals( 0, $metric_values[0]->getValue() );
$this->assertNull( $metric_values[1]->getValue() );
}
}

/**
* @dataProvider data_access_token
*
Expand Down