├── LICENSE ├── README.md ├── test.php └── universal-analytics.php /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2013-2014, Adswerve, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Analytics for PHP 2 | 3 | This library provides a PHP interface to Google Analytics, supporting the Universal Analytics Measurement Protocol, with an interface modeled (loosely) after Google's `analytics.js`. 4 | Future releases will support an interface similar to `ga.js`, for legacy implementations which already integrate our [legacy library](https://github.com/analytics-pros/google-analytics-php-legacy). 5 | 6 | **NOTE** that this project is still _beta_; some features of the Measurement Protocol aren't fully represented, and new features will be added in the (hopefully) nearer future. Please feel free to file issues for feature requests. 7 | 8 | # Contact 9 | Email: `opensource@adswerve.com` 10 | 11 | # Usage 12 | 13 | For the most accurate data in your reports, Analytics Pros recommends establishing a distinct ID for each of your users, and integrating that ID on your front-end web tracking, as well as back-end tracking calls. This provides for a consistent, correct representation of user engagement, without skewing overall visit metrics (and others). 14 | 15 | A few simple examples: 16 | 17 | ```php 18 | set('dimension1', 'pizza'); 26 | 27 | // Send an event 28 | $t->send(/* hit type */ 'event', /* hit properties */ array( 29 | 'eventCategory' => 'test events', 30 | 'eventAction' => 'testing', 31 | 'eventLabel' => '(test)' 32 | )); 33 | 34 | 35 | // Send a transaction 36 | $tracker->send('transaction', array( 37 | 'transactionId' => $transaction_id, 38 | 'transactionAffiliation' => $affiliate, 39 | 'transactionRevenue' => $total_revenue, // not including tax or shipping 40 | 'transactionShipping' => $total_shipping, 41 | 'transactionTax' => $total_tax 42 | )); 43 | 44 | // Send an item record related to the preceding transaction 45 | $tracker->send('item', array( 46 | 'transactionId' => $transaction_id, 47 | 'itemName' => $item_name, 48 | 'itemCode' => $item_sku, 49 | 'itemCategory' => $item_variation, 50 | 'itemPrice' => $item_unit_price, 51 | 'itemQuantity' => 1 52 | )); 53 | 54 | 55 | ?> 56 | ``` 57 | 58 | All messages will be flushed when the tracker object is destroyed. 59 | 60 | Currently all tracking hits (using `send`) require an array (dictionary) of properties related to the hit type. 61 | 62 | 63 | # Features not implemented 64 | 65 | * Throttling 66 | * GA Classic interface 67 | 68 | We're particularly interested in the scope of throttling for back-end tracking for users who have a defined use-case for it. Please [contact us](mailto:opensource@adswerve.com) if you have such a use-case. 69 | 70 | 71 | # License 72 | 73 | universal-analytics-php is licensed under the [BSD license](./LICENSE) 74 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | set('dimension1', 'tested'); 9 | 10 | 11 | if(true) $t->send('event', array( 12 | 'eventAction' => 'testing', 13 | 'eventCategory' => 'test events', 14 | 'eventLabel' => '(test)' 15 | )); 16 | 17 | ?> 18 | -------------------------------------------------------------------------------- /universal-analytics.php: -------------------------------------------------------------------------------- 1 | data = $data; 21 | $this->user_agent = $user_agent; 22 | } 23 | 24 | public function handle($data_update = null, $debug = false){ 25 | $data = array_merge($this->data, (array)$data_update); 26 | return self::curl($this->endpoint, $data, $this->user_agent, $debug); 27 | } 28 | 29 | // Issue an HTTP request via CURL 30 | public static function & curl($url, $data, $ua = null, $debug = true){ 31 | $h = curl_init($url); 32 | $payload = self::combine($data); 33 | curl_setopt($h, CURLOPT_AUTOREFERER, true); 34 | curl_setopt($h, CURLOPT_NOPROGRESS, true); 35 | if($debug){ 36 | print_r($data); 37 | print_r($payload); 38 | print "\n"; # readability 39 | } 40 | if(is_string($ua)){ 41 | curl_setopt($h, CURLOPT_USERAGENT, $ua); 42 | } 43 | curl_setopt($h, CURLOPT_VERBOSE, $debug); 44 | curl_setopt($h, CURLOPT_POST, count($data)); 45 | curl_setopt($h, CURLOPT_POSTFIELDS, $payload); 46 | curl_setopt($h, CURLOPT_RETURNTRANSFER, true); 47 | curl_setopt($h, CURLOPT_HEADER, 0); 48 | return $h; 49 | // $v = curl_exec($h); 50 | // curl_close($h); 51 | // return $v; 52 | } 53 | 54 | // A simpler parameter joining method 55 | public static function combine($params, $pair = '=', $sep = '&'){ 56 | $c = count($params); 57 | return $c ? implode($sep, array_map( 58 | 'sprintf', // NOTE: even built-in functions require names given as strings when mapping 59 | array_fill(0, $c, '%s%s%s'), // format string 60 | array_keys($params), // keys 61 | array_fill(0, $c, $pair), // pairing (=) 62 | array_map('urlencode', array_values($params)) // values 63 | )) : ''; 64 | } 65 | 66 | } 67 | 68 | 69 | class UniversalBeaconPool { 70 | const MAXIMUM_REQUEST_QUEUE = 10; 71 | private $user_agent = 'user_agent'; 72 | private $debug = false; 73 | private $handler = null; 74 | private $request_queue = array(); 75 | 76 | public function __construct($user_agent = null, $debug = false){ 77 | $this->handler = curl_multi_init(); 78 | 79 | // This option isn't supported before PHP 5.5 80 | // curl_multi_setopt($this->handler, CURLMOPT_PIPELINING, 1); 81 | 82 | if(is_string($user_agent)){ 83 | $this->user_agent = $user_agent; 84 | } 85 | if($debug){ 86 | $this->debug = true; 87 | } 88 | } 89 | 90 | 91 | public function addRequest($data, $user_agent = null){ 92 | $user_agent = (is_string($user_agent) ? $user_agent : $this->user_agent); 93 | $request = new UniversalBeacon($data, $user_agent); 94 | $handle = $request->handle(null, $this->debug); 95 | array_push($this->request_queue, $handle); 96 | curl_multi_add_handle($this->handler, $handle); 97 | if(count($this->request_queue) >= self::MAXIMUM_REQUEST_QUEUE){ 98 | self::process($this->handler, $this->request_queue); 99 | } 100 | } 101 | 102 | public static function process(& $handler, & $request_queue){ 103 | do { 104 | curl_multi_exec($handler, $running); 105 | } while($running > 0); 106 | while($handle = array_pop($request_queue)){ 107 | curl_multi_remove_handle($handler, $handle); 108 | } 109 | } 110 | 111 | 112 | public function __destruct(){ 113 | self::process($this->handler, $this->request_queue); 114 | curl_multi_close($this->handler); 115 | } 116 | 117 | } 118 | 119 | 120 | class Tracker { 121 | const VERSION = 1; 122 | const USER_AGENT = 'Analytics Pros - Universal Analytics (PHP)'; 123 | private $account = null; 124 | private $state = null; 125 | private $user_agent = null; 126 | private $pool = null; 127 | public $debug = false; 128 | 129 | public function __construct($account, $client_id = null, $user_id = null, $debug = false){ 130 | $this->account = $account; 131 | $this->debug = (bool) $debug; 132 | $this->pool = new UniversalBeaconPool(self::USER_AGENT, $this->debug); 133 | 134 | if(!is_null($client_id) && constant('ANALYTICS_HASH_IDS')) 135 | $client_id = self::hash_uuid($client_id); 136 | elseif(is_null($client_id)) 137 | $client_id = self::generateUUID4(); 138 | 139 | $this->state = array( 140 | 'v' => self::VERSION, 141 | 'tid' => $account, 142 | 'cid' => $client_id 143 | ); 144 | 145 | if(!is_null($user_id)){ 146 | if(constant('ANALYTICS_HASH_IDS')) 147 | $user_id = self::hash_uuid($user_id); 148 | $this->state[ 'uid' ] = $user_id; 149 | } 150 | } 151 | 152 | /* Return an MD5 checksum spaced in UUD4-format */ 153 | public static function hash_uuid($value){ 154 | $checksum = md5($value); 155 | return sprintf('%8s-%4s-%4s-%4s-%12s', 156 | substr($checksum, 0, 8), 157 | substr($checksum, 8, 4), 158 | substr($checksum, 12, 4), 159 | substr($checksum, 16, 4), 160 | substr($checksum, 20, 12) 161 | ); 162 | } 163 | 164 | 165 | public function setUserAgent($ua){ 166 | if(is_string($ua)){ 167 | $this->user_agent = $ua; 168 | } 169 | } 170 | 171 | public function send($hit_type, $attribs = null, $ua = null){ 172 | $agent = (is_string($ua) 173 | ? $ua 174 | : (is_string($this->user_agent) 175 | ? $this->user_agent 176 | : self::USER_AGENT 177 | ) 178 | ); 179 | 180 | return $this->pool->addRequest($this->hitdata($hit_type, $attribs), $agent); 181 | } 182 | 183 | public function set($name, $value){ 184 | $this->state[ $name ] = $value; 185 | } 186 | 187 | public function get($name){ 188 | if(array_key_exists($name, $this->state)){ 189 | return $this->state[ $name ]; 190 | } else { 191 | return null; 192 | } 193 | } 194 | 195 | public function hitdata($type, $attribs = null){ 196 | return self::params($type, array_merge($this->state, (array)$attribs)); 197 | } 198 | 199 | public static function & params($type, $data){ 200 | $result_data = array(); 201 | $result_keys_in = array_keys($data); 202 | $result_keys = str_replace(array_keys(self::$name_map), array_values(self::$name_map), $result_keys_in); 203 | $result_keys = preg_replace(array_keys(self::$name_map_re), array_values(self::$name_map_re), $result_keys); 204 | for($i = 0; $i < count($result_keys_in); $i++){ 205 | $result_data[ $result_keys[ $i ] ] = $data[ $result_keys_in[ $i ] ]; 206 | } 207 | $result_data[ 't' ] = $type; 208 | return $result_data; 209 | } 210 | 211 | public static $name_map = array( 212 | 'clientId' => 'cid', 213 | 'userId' => 'uid', 214 | 'eventCategory' => 'ec', 215 | 'eventAction' => 'ea', 216 | 'eventLabel' => 'el', 217 | 'eventValue' => 'ev', 218 | 'nonInteraction' => 'ni', 219 | 'nonInteractive' => 'ni', 220 | 'documentPath' => 'dp', 221 | 'documentTitle' => 'dt', 222 | 'title' => 'dt', 223 | 'path' => 'dp', 224 | 'page' => 'dp', 225 | 'location' => 'dl', 226 | 'documentLocation' => 'dl', 227 | 'hostname' => 'dh', 228 | 'documentHostname' => 'dh', 229 | 'sessionControl' => 'sc', 230 | 'referrer' => 'dr', 231 | 'documentReferrer' => 'dr', 232 | 'queueTime' => 'qt', 233 | 'campaignName' => 'cn', 234 | 'campaignSource' => 'cs', 235 | 'campaignMedium' => 'cm', 236 | 'campaignKeyword' => 'ck', 237 | 'campaignContent' => 'cc', 238 | 'campaignId' => 'ci', 239 | 'screenResolution' => 'sr', 240 | 'viewportSize' => 'vp', 241 | 'documentEncoding' => 'de', 242 | 'screenColors' => 'sd', 243 | 'userLanguage' => 'ul', 244 | 'appName' => 'an', 245 | 'contentDescription' => 'cd', 246 | 'appVersion' => 'av', 247 | 'transactionAffiliation' => 'ta', 248 | 'transactionId' => 'ti', 249 | 'transactionRevenue' => 'tr', 250 | 'transactionShipping' => 'ts', 251 | 'transactionTax' => 'tt', 252 | 'transactionCurrency' => 'cu', 253 | 'itemName' => 'in', 254 | 'itemPrice' => 'ip', 255 | 'itemQuantity' => 'iq', 256 | 'itemCode' => 'ic', 257 | 'itemVariation' => 'iv', 258 | 'itemCategory' => 'iv', 259 | 'socialAction' => 'sa', 260 | 'socialNetwork' => 'sn', 261 | 'socialTarget' => 'st', 262 | 'exceptionDescription' => 'exd', 263 | 'exceptionFatal' => 'exf', 264 | 'timingCategory' => 'utc', 265 | 'timingVariable' => 'utv', 266 | 'timingTime' => 'utt', 267 | 'timingLabel' => 'utl', 268 | 'timingDNS' => 'dns', 269 | 'timingPageLoad' => 'pdt', 270 | 'timingRedirect' => 'rrt', 271 | 'timingTCPConnect' => 'tcp', 272 | 'timingServerResponse' => 'srt' 273 | ); 274 | 275 | public static $name_map_re = array( 276 | '@^dimension([0-9]+)$@' => 'cd$1', 277 | '@^metric([0-9]+)$@' => 'cm$1' 278 | ); 279 | 280 | public static function generateUUID4(){ 281 | return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 282 | // 32 bits for "time_low" 283 | mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), 284 | 285 | // 16 bits for "time_mid" 286 | mt_rand( 0, 0xffff ), 287 | 288 | // 16 bits for "time_hi_and_version", 289 | // four most significant bits holds version number 4 290 | mt_rand( 0, 0x0fff ) | 0x4000, 291 | 292 | // 16 bits, 8 bits for "clk_seq_hi_res", 293 | // 8 bits for "clk_seq_low", 294 | // two most significant bits holds zero and one for variant DCE1.1 295 | mt_rand( 0, 0x3fff ) | 0x8000, 296 | 297 | // 48 bits for "node" 298 | mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) 299 | ); 300 | } 301 | 302 | } 303 | 304 | ?> 305 | --------------------------------------------------------------------------------