Skip to content

Commit

Permalink
Merge pull request #90 from theiconic/feature/batch-requests
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgeborges authored Sep 24, 2020
2 parents 0797dcc + af8bdc5 commit d02559c
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 20 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,58 @@ $analytics->setEventCategory('Checkout')
->sendEvent();
```

### Batch Hits

GA has an endpoint that you can hit to register multiple hits at once, with a limit of 20 hits. Hits to be send can be placed in a queue as you build up the Analytics object.

Here's an example that sends two hits, and then empties the queue.

```php
$analytics = new Analytics(false, false);

$analytics
->setProtocolVersion('1')
->setTrackingId('UA-xxxxxx-x')
->setClientId('xxxxxx.xxxxxx');

foreach(range(0, 19) as $i) {
$analytics = $analytics
->setDocumentPath("/mypage$i")
->enqueuePageview(); //enqueue url without pushing
}

$analytics->sendEnqueuedHits(); //push 20 pageviews in a single request and empties the queue
```

The queue is emptied when the hits are sent, but it can also be empty manually with `emptyQueue` method.

```php
$analytics = new Analytics(false, false);

$analytics
->setProtocolVersion('1')
->setTrackingId('UA-xxxxxx-x')
->setClientId('xxxxxx.xxxxxx');

foreach(range(0, 5) as $i) {
$analytics = $analytics
->setDocumentPath("/mypage$i")
->enqueuePageview(); //enqueue url without pushing
}

$analytics->emptyQueue(); // empty queue, allows to enqueue 20 hits again

foreach(range(1, 20) as $i) {
$analytics = $analytics
->setDocumentPath("/mypage$i")
->enqueuePageview(); //enqueue url without pushing
}

$analytics->sendEnqueuedHits(); //push 20 pageviews in a single request and empties the queue
```

If more than 20 hits are attempted to be enqueue, the library will throw a `EnqueueUrlsOverflowException`.

### Validating Hits

From Google Developer Guide:
Expand Down
130 changes: 113 additions & 17 deletions src/Analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

namespace TheIconic\Tracking\GoogleAnalytics;

use TheIconic\Tracking\GoogleAnalytics\Parameters\SingleParameter;
use TheIconic\Tracking\GoogleAnalytics\Parameters\CompoundParameterCollection;
use BadMethodCallException;
use TheIconic\Tracking\GoogleAnalytics\Exception\EnqueueUrlsOverflowException;
use TheIconic\Tracking\GoogleAnalytics\Exception\InvalidPayloadDataException;
use TheIconic\Tracking\GoogleAnalytics\Network\HttpClient;
use TheIconic\Tracking\GoogleAnalytics\Network\PrepareUrl;
use TheIconic\Tracking\GoogleAnalytics\Exception\InvalidPayloadDataException;
use TheIconic\Tracking\GoogleAnalytics\Parameters\CompoundParameterCollection;
use TheIconic\Tracking\GoogleAnalytics\Parameters\SingleParameter;

/**
* Class Analytics
Expand Down Expand Up @@ -319,6 +321,14 @@ class Analytics
*/
protected $debugEndpoint = '://www.google-analytics.com/debug/collect';

/**
* Endpoint to connect to when sending batch data to GA.
*
* @var string
*/
protected $batchEndpoint = '://www.google-analytics.com/batch';


/**
* Indicates if the request is in debug mode(validating hits).
*
Expand Down Expand Up @@ -354,6 +364,11 @@ class Analytics
*/
protected $isDisabled = false;

/**
* @var array
*/
protected $enqueuedUrls = [];

/**
* @var array
*/
Expand Down Expand Up @@ -553,6 +568,16 @@ protected function getEndpoint()
return ($this->isDebug) ? $this->uriScheme . $this->debugEndpoint : $this->uriScheme . $this->endpoint;
}

/**
* Gets the full batch endpoint to GA.
*
* @return string
*/
protected function getBatchEndpoint()
{
return $this->uriScheme . $this->batchEndpoint;
}

/**
* Sets debug mode to true or false.
*
Expand All @@ -578,6 +603,47 @@ protected function sendHit($methodName)
{
$hitType = strtoupper(substr($methodName, 4));

$this->setAndValidateHit($hitType);

if ($this->isDisabled) {
return new NullAnalyticsResponse();
}

return $this->getHttpClient()->post($this->getUrl(), $this->getHttpClientOptions());
}

/**
* Enqueue a hit to GA. The hit will contain in the payload all the parameters added before.
*
* @param $methodName
* @return $this
* @throws Exception\InvalidPayloadDataException
*/
protected function enqueueHit($methodName)
{

if(count($this->enqueuedUrls) == 20) {
throw new EnqueueUrlsOverflowException();
}

$hitType = strtoupper(substr($methodName, 7));

$this->setAndValidateHit($hitType);
$this->enqueuedUrls[] = $this->getUrl(true);

return $this;
}

/**
* Validate and set hitType
*
* @param $methodName
* @return void
* @throws Exception\InvalidPayloadDataException
*/
protected function setAndValidateHit($hitType)
{

$hitConstant = $this->getParameterClassConstant(
'TheIconic\Tracking\GoogleAnalytics\Parameters\Hit\HitType::HIT_TYPE_' . $hitType,
'Hit type ' . $hitType . ' is not defined, check spelling'
Expand All @@ -588,12 +654,24 @@ protected function sendHit($methodName)
if (!$this->hasMinimumRequiredParameters()) {
throw new InvalidPayloadDataException();
}
}

/**
* Sends enqueued hits to GA. These hits will contain in the payload all the parameters added before.
*
* @return AnalyticsResponseInterface
*/
public function sendEnqueuedHits()
{
if ($this->isDisabled) {
return new NullAnalyticsResponse();
}

return $this->getHttpClient()->post($this->getUrl(), $this->getHttpClientOptions());
$response = $this->getHttpClient()->batch($this->getBatchEndpoint(), $this->enqueuedUrls, $this->getHttpClientOptions());

$this->emptyQueue();

return $response;
}

/**
Expand All @@ -618,14 +696,15 @@ protected function getHttpClientOptions()
* @api
* @return string
*/
public function getUrl()
public function getUrl($onlyQuery = false)
{
$prepareUrl = new PrepareUrl;

return $prepareUrl->build(
$this->getEndpoint(),
$this->singleParameters,
$this->compoundParametersCollections
$this->compoundParametersCollections,
$onlyQuery
);
}

Expand Down Expand Up @@ -691,14 +770,14 @@ protected function setParameterActionTo($parameter, $action)
* @param $constant
* @param $exceptionMsg
* @return mixed
* @throws \BadMethodCallException
* @throws BadMethodCallException
*/
protected function getParameterClassConstant($constant, $exceptionMsg)
{
if (defined($constant)) {
return constant($constant);
} else {
throw new \BadMethodCallException($exceptionMsg);
throw new BadMethodCallException($exceptionMsg);
}
}

Expand Down Expand Up @@ -760,8 +839,9 @@ protected function addItem($methodName, array $methodArguments)

$collectionIndex = $this->getIndexFromArguments($methodArguments);

if (isset($this->compoundParametersCollections[$parameterClass . $collectionIndex])) {
$this->compoundParametersCollections[$parameterClass . $collectionIndex]->add($parameterObject);
$parameterIndex = $parameterClass . $collectionIndex;
if (isset($this->compoundParametersCollections[$parameterIndex])) {
$this->compoundParametersCollections[$parameterIndex]->add($parameterObject);
} else {
$fullParameterCollectionClass = $fullParameterClass . 'Collection';

Expand All @@ -770,7 +850,7 @@ protected function addItem($methodName, array $methodArguments)

$parameterObjectCollection->add($parameterObject);

$this->compoundParametersCollections[$parameterClass . $collectionIndex] = $parameterObjectCollection;
$this->compoundParametersCollections[$parameterIndex] = $parameterObjectCollection;
}

return $this;
Expand Down Expand Up @@ -847,15 +927,27 @@ protected function getIndexFromArguments($methodArguments)
* @param $parameterClass
* @param $methodName
* @return string
* @throws \BadMethodCallException
* @throws BadMethodCallException
*/
protected function getFullParameterClass($parameterClass, $methodName)
{
if (empty($this->availableParameters[$parameterClass])) {
throw new \BadMethodCallException('Method ' . $methodName . ' not defined for Analytics class');
} else {
return '\\TheIconic\\Tracking\\GoogleAnalytics\\Parameters\\' . $this->availableParameters[$parameterClass];
throw new BadMethodCallException('Method ' . $methodName . ' not defined for Analytics class');
}

return '\\TheIconic\\Tracking\\GoogleAnalytics\\Parameters\\' . $this->availableParameters[$parameterClass];
}

/**
* Empty batch queue
*
* @return $this
*/
public function emptyQueue()
{
$this->enqueuedUrls = [];

return $this;
}

/**
Expand All @@ -864,7 +956,7 @@ protected function getFullParameterClass($parameterClass, $methodName)
* @param $methodName
* @param array $methodArguments
* @return mixed
* @throws \BadMethodCallException
* @throws BadMethodCallException
*/
public function __call($methodName, array $methodArguments)
{
Expand All @@ -886,12 +978,16 @@ public function __call($methodName, array $methodArguments)
return $this->sendHit($methodName);
}

if (preg_match('/^(enqueue)(\w+)/', $methodName, $matches)) {
return $this->enqueueHit($methodName);
}

// Get Parameters
if (preg_match('/^(get)(\w+)/', $methodName, $matches)) {
return $this->getParameter($methodName, $methodArguments);
}

throw new \BadMethodCallException('Method ' . $methodName . ' not defined for Analytics class');
throw new BadMethodCallException('Method ' . $methodName . ' not defined for Analytics class');
}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/Exception/EnqueueUrlsOverflowException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace TheIconic\Tracking\GoogleAnalytics\Exception;

/**
* Class InvalidPayloadDataException
*
* Thrown when a hit is tried to be sent and the minimum requirements for parameters are not met.
*
* @package TheIconic\Tracking\GoogleAnalytics\Exception
*/
class EnqueueUrlsOverflowException extends \OverflowException
{
/**
* @var string
*/
protected $message = 'A maximum of 20 hits can be specified per request.';
}
28 changes: 28 additions & 0 deletions src/Network/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,34 @@ public function post($url, array $options = [])
['User-Agent' => self::PHP_GA_MEASUREMENT_PROTOCOL_USER_AGENT]
);

return $this->sendRequest($request, $options);
}

/**
* Sends batch request to Google Analytics.
*
* @internal
* @param string $url
* @param array $batchUrls
* @param array $options
* @return AnalyticsResponse
*/
public function batch($url, array $batchUrls, array $options = [])
{
$body = implode(PHP_EOL, $batchUrls);

$request = new Request(
'POST',
$url,
['User-Agent' => self::PHP_GA_MEASUREMENT_PROTOCOL_USER_AGENT],
$body
);

return $this->sendRequest($request, $options);
}

private function sendRequest(Request $request, array $options = [])
{
$opts = $this->parseOptions($options);
$response = $this->getClient()->sendAsync($request, [
'synchronous' => !$opts['async'],
Expand Down
4 changes: 2 additions & 2 deletions src/Network/PrepareUrl.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class PrepareUrl
* @param CompoundParameterCollection[] $compoundParameters
* @return string
*/
public function build($url, array $singleParameters, array $compoundParameters)
public function build($url, array $singleParameters, array $compoundParameters, $onlyQuery = false)
{
$singlesPost = $this->getSingleParametersPayload($singleParameters);

Expand All @@ -46,7 +46,7 @@ public function build($url, array $singleParameters, array $compoundParameters)
$this->payloadParameters['z'] = $this->cacheBuster;
}
$query = http_build_query($this->payloadParameters, null, ini_get('arg_separator.output'), PHP_QUERY_RFC3986);
return $url . '?' . $query;
return $onlyQuery ? $query : ($url . '?' . $query);
}

/**
Expand Down
Loading

0 comments on commit d02559c

Please sign in to comment.