Skip to content

Commit d02559c

Browse files
authored
Merge pull request #90 from theiconic/feature/batch-requests
2 parents 0797dcc + af8bdc5 commit d02559c

File tree

7 files changed

+404
-20
lines changed

7 files changed

+404
-20
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,58 @@ $analytics->setEventCategory('Checkout')
197197
->sendEvent();
198198
```
199199

200+
### Batch Hits
201+
202+
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.
203+
204+
Here's an example that sends two hits, and then empties the queue.
205+
206+
```php
207+
$analytics = new Analytics(false, false);
208+
209+
$analytics
210+
->setProtocolVersion('1')
211+
->setTrackingId('UA-xxxxxx-x')
212+
->setClientId('xxxxxx.xxxxxx');
213+
214+
foreach(range(0, 19) as $i) {
215+
$analytics = $analytics
216+
->setDocumentPath("/mypage$i")
217+
->enqueuePageview(); //enqueue url without pushing
218+
}
219+
220+
$analytics->sendEnqueuedHits(); //push 20 pageviews in a single request and empties the queue
221+
```
222+
223+
The queue is emptied when the hits are sent, but it can also be empty manually with `emptyQueue` method.
224+
225+
```php
226+
$analytics = new Analytics(false, false);
227+
228+
$analytics
229+
->setProtocolVersion('1')
230+
->setTrackingId('UA-xxxxxx-x')
231+
->setClientId('xxxxxx.xxxxxx');
232+
233+
foreach(range(0, 5) as $i) {
234+
$analytics = $analytics
235+
->setDocumentPath("/mypage$i")
236+
->enqueuePageview(); //enqueue url without pushing
237+
}
238+
239+
$analytics->emptyQueue(); // empty queue, allows to enqueue 20 hits again
240+
241+
foreach(range(1, 20) as $i) {
242+
$analytics = $analytics
243+
->setDocumentPath("/mypage$i")
244+
->enqueuePageview(); //enqueue url without pushing
245+
}
246+
247+
$analytics->sendEnqueuedHits(); //push 20 pageviews in a single request and empties the queue
248+
```
249+
250+
If more than 20 hits are attempted to be enqueue, the library will throw a `EnqueueUrlsOverflowException`.
251+
200252
### Validating Hits
201253

202254
From Google Developer Guide:

src/Analytics.php

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
namespace TheIconic\Tracking\GoogleAnalytics;
44

5-
use TheIconic\Tracking\GoogleAnalytics\Parameters\SingleParameter;
6-
use TheIconic\Tracking\GoogleAnalytics\Parameters\CompoundParameterCollection;
5+
use BadMethodCallException;
6+
use TheIconic\Tracking\GoogleAnalytics\Exception\EnqueueUrlsOverflowException;
7+
use TheIconic\Tracking\GoogleAnalytics\Exception\InvalidPayloadDataException;
78
use TheIconic\Tracking\GoogleAnalytics\Network\HttpClient;
89
use TheIconic\Tracking\GoogleAnalytics\Network\PrepareUrl;
9-
use TheIconic\Tracking\GoogleAnalytics\Exception\InvalidPayloadDataException;
10+
use TheIconic\Tracking\GoogleAnalytics\Parameters\CompoundParameterCollection;
11+
use TheIconic\Tracking\GoogleAnalytics\Parameters\SingleParameter;
1012

1113
/**
1214
* Class Analytics
@@ -319,6 +321,14 @@ class Analytics
319321
*/
320322
protected $debugEndpoint = '://www.google-analytics.com/debug/collect';
321323

324+
/**
325+
* Endpoint to connect to when sending batch data to GA.
326+
*
327+
* @var string
328+
*/
329+
protected $batchEndpoint = '://www.google-analytics.com/batch';
330+
331+
322332
/**
323333
* Indicates if the request is in debug mode(validating hits).
324334
*
@@ -354,6 +364,11 @@ class Analytics
354364
*/
355365
protected $isDisabled = false;
356366

367+
/**
368+
* @var array
369+
*/
370+
protected $enqueuedUrls = [];
371+
357372
/**
358373
* @var array
359374
*/
@@ -553,6 +568,16 @@ protected function getEndpoint()
553568
return ($this->isDebug) ? $this->uriScheme . $this->debugEndpoint : $this->uriScheme . $this->endpoint;
554569
}
555570

571+
/**
572+
* Gets the full batch endpoint to GA.
573+
*
574+
* @return string
575+
*/
576+
protected function getBatchEndpoint()
577+
{
578+
return $this->uriScheme . $this->batchEndpoint;
579+
}
580+
556581
/**
557582
* Sets debug mode to true or false.
558583
*
@@ -578,6 +603,47 @@ protected function sendHit($methodName)
578603
{
579604
$hitType = strtoupper(substr($methodName, 4));
580605

606+
$this->setAndValidateHit($hitType);
607+
608+
if ($this->isDisabled) {
609+
return new NullAnalyticsResponse();
610+
}
611+
612+
return $this->getHttpClient()->post($this->getUrl(), $this->getHttpClientOptions());
613+
}
614+
615+
/**
616+
* Enqueue a hit to GA. The hit will contain in the payload all the parameters added before.
617+
*
618+
* @param $methodName
619+
* @return $this
620+
* @throws Exception\InvalidPayloadDataException
621+
*/
622+
protected function enqueueHit($methodName)
623+
{
624+
625+
if(count($this->enqueuedUrls) == 20) {
626+
throw new EnqueueUrlsOverflowException();
627+
}
628+
629+
$hitType = strtoupper(substr($methodName, 7));
630+
631+
$this->setAndValidateHit($hitType);
632+
$this->enqueuedUrls[] = $this->getUrl(true);
633+
634+
return $this;
635+
}
636+
637+
/**
638+
* Validate and set hitType
639+
*
640+
* @param $methodName
641+
* @return void
642+
* @throws Exception\InvalidPayloadDataException
643+
*/
644+
protected function setAndValidateHit($hitType)
645+
{
646+
581647
$hitConstant = $this->getParameterClassConstant(
582648
'TheIconic\Tracking\GoogleAnalytics\Parameters\Hit\HitType::HIT_TYPE_' . $hitType,
583649
'Hit type ' . $hitType . ' is not defined, check spelling'
@@ -588,12 +654,24 @@ protected function sendHit($methodName)
588654
if (!$this->hasMinimumRequiredParameters()) {
589655
throw new InvalidPayloadDataException();
590656
}
657+
}
591658

659+
/**
660+
* Sends enqueued hits to GA. These hits will contain in the payload all the parameters added before.
661+
*
662+
* @return AnalyticsResponseInterface
663+
*/
664+
public function sendEnqueuedHits()
665+
{
592666
if ($this->isDisabled) {
593667
return new NullAnalyticsResponse();
594668
}
595669

596-
return $this->getHttpClient()->post($this->getUrl(), $this->getHttpClientOptions());
670+
$response = $this->getHttpClient()->batch($this->getBatchEndpoint(), $this->enqueuedUrls, $this->getHttpClientOptions());
671+
672+
$this->emptyQueue();
673+
674+
return $response;
597675
}
598676

599677
/**
@@ -618,14 +696,15 @@ protected function getHttpClientOptions()
618696
* @api
619697
* @return string
620698
*/
621-
public function getUrl()
699+
public function getUrl($onlyQuery = false)
622700
{
623701
$prepareUrl = new PrepareUrl;
624702

625703
return $prepareUrl->build(
626704
$this->getEndpoint(),
627705
$this->singleParameters,
628-
$this->compoundParametersCollections
706+
$this->compoundParametersCollections,
707+
$onlyQuery
629708
);
630709
}
631710

@@ -691,14 +770,14 @@ protected function setParameterActionTo($parameter, $action)
691770
* @param $constant
692771
* @param $exceptionMsg
693772
* @return mixed
694-
* @throws \BadMethodCallException
773+
* @throws BadMethodCallException
695774
*/
696775
protected function getParameterClassConstant($constant, $exceptionMsg)
697776
{
698777
if (defined($constant)) {
699778
return constant($constant);
700779
} else {
701-
throw new \BadMethodCallException($exceptionMsg);
780+
throw new BadMethodCallException($exceptionMsg);
702781
}
703782
}
704783

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

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

763-
if (isset($this->compoundParametersCollections[$parameterClass . $collectionIndex])) {
764-
$this->compoundParametersCollections[$parameterClass . $collectionIndex]->add($parameterObject);
842+
$parameterIndex = $parameterClass . $collectionIndex;
843+
if (isset($this->compoundParametersCollections[$parameterIndex])) {
844+
$this->compoundParametersCollections[$parameterIndex]->add($parameterObject);
765845
} else {
766846
$fullParameterCollectionClass = $fullParameterClass . 'Collection';
767847

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

771851
$parameterObjectCollection->add($parameterObject);
772852

773-
$this->compoundParametersCollections[$parameterClass . $collectionIndex] = $parameterObjectCollection;
853+
$this->compoundParametersCollections[$parameterIndex] = $parameterObjectCollection;
774854
}
775855

776856
return $this;
@@ -847,15 +927,27 @@ protected function getIndexFromArguments($methodArguments)
847927
* @param $parameterClass
848928
* @param $methodName
849929
* @return string
850-
* @throws \BadMethodCallException
930+
* @throws BadMethodCallException
851931
*/
852932
protected function getFullParameterClass($parameterClass, $methodName)
853933
{
854934
if (empty($this->availableParameters[$parameterClass])) {
855-
throw new \BadMethodCallException('Method ' . $methodName . ' not defined for Analytics class');
856-
} else {
857-
return '\\TheIconic\\Tracking\\GoogleAnalytics\\Parameters\\' . $this->availableParameters[$parameterClass];
935+
throw new BadMethodCallException('Method ' . $methodName . ' not defined for Analytics class');
858936
}
937+
938+
return '\\TheIconic\\Tracking\\GoogleAnalytics\\Parameters\\' . $this->availableParameters[$parameterClass];
939+
}
940+
941+
/**
942+
* Empty batch queue
943+
*
944+
* @return $this
945+
*/
946+
public function emptyQueue()
947+
{
948+
$this->enqueuedUrls = [];
949+
950+
return $this;
859951
}
860952

861953
/**
@@ -864,7 +956,7 @@ protected function getFullParameterClass($parameterClass, $methodName)
864956
* @param $methodName
865957
* @param array $methodArguments
866958
* @return mixed
867-
* @throws \BadMethodCallException
959+
* @throws BadMethodCallException
868960
*/
869961
public function __call($methodName, array $methodArguments)
870962
{
@@ -886,12 +978,16 @@ public function __call($methodName, array $methodArguments)
886978
return $this->sendHit($methodName);
887979
}
888980

981+
if (preg_match('/^(enqueue)(\w+)/', $methodName, $matches)) {
982+
return $this->enqueueHit($methodName);
983+
}
984+
889985
// Get Parameters
890986
if (preg_match('/^(get)(\w+)/', $methodName, $matches)) {
891987
return $this->getParameter($methodName, $methodArguments);
892988
}
893989

894-
throw new \BadMethodCallException('Method ' . $methodName . ' not defined for Analytics class');
990+
throw new BadMethodCallException('Method ' . $methodName . ' not defined for Analytics class');
895991
}
896992

897993
/**
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace TheIconic\Tracking\GoogleAnalytics\Exception;
4+
5+
/**
6+
* Class InvalidPayloadDataException
7+
*
8+
* Thrown when a hit is tried to be sent and the minimum requirements for parameters are not met.
9+
*
10+
* @package TheIconic\Tracking\GoogleAnalytics\Exception
11+
*/
12+
class EnqueueUrlsOverflowException extends \OverflowException
13+
{
14+
/**
15+
* @var string
16+
*/
17+
protected $message = 'A maximum of 20 hits can be specified per request.';
18+
}

src/Network/HttpClient.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,34 @@ public function post($url, array $options = [])
9494
['User-Agent' => self::PHP_GA_MEASUREMENT_PROTOCOL_USER_AGENT]
9595
);
9696

97+
return $this->sendRequest($request, $options);
98+
}
99+
100+
/**
101+
* Sends batch request to Google Analytics.
102+
*
103+
* @internal
104+
* @param string $url
105+
* @param array $batchUrls
106+
* @param array $options
107+
* @return AnalyticsResponse
108+
*/
109+
public function batch($url, array $batchUrls, array $options = [])
110+
{
111+
$body = implode(PHP_EOL, $batchUrls);
112+
113+
$request = new Request(
114+
'POST',
115+
$url,
116+
['User-Agent' => self::PHP_GA_MEASUREMENT_PROTOCOL_USER_AGENT],
117+
$body
118+
);
119+
120+
return $this->sendRequest($request, $options);
121+
}
122+
123+
private function sendRequest(Request $request, array $options = [])
124+
{
97125
$opts = $this->parseOptions($options);
98126
$response = $this->getClient()->sendAsync($request, [
99127
'synchronous' => !$opts['async'],

src/Network/PrepareUrl.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class PrepareUrl
3434
* @param CompoundParameterCollection[] $compoundParameters
3535
* @return string
3636
*/
37-
public function build($url, array $singleParameters, array $compoundParameters)
37+
public function build($url, array $singleParameters, array $compoundParameters, $onlyQuery = false)
3838
{
3939
$singlesPost = $this->getSingleParametersPayload($singleParameters);
4040

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

5252
/**

0 commit comments

Comments
 (0)