├── .gitignore ├── README.md ├── api ├── controllers │ ├── create.js │ ├── javascript.js │ └── send.js └── helpers │ └── cloudwatch.js ├── app.js ├── config └── ga_config.js ├── context.json ├── deploy.env ├── event.json ├── event_sources.json ├── index.js ├── package.json ├── static └── collect.gif ├── swagger └── v0.1.json └── views └── gap.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE files 2 | .idea 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | dist/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Runtime configuration for swagger app 31 | config/runtime.yaml 32 | 33 | # AWS credentials 34 | .env 35 | 36 | # NYPL Simplified's Card Creator API credentials 37 | config/ccConfig.js 38 | 39 | # Built files 40 | dist 41 | 42 | # Other files to be ignored 43 | npm-debug.log 44 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Analytics Proxy 2 | 3 | Google Analytics Proxy (GAP) provides a Proxy Service to Google's [Measurement Protocol](https://developers.google.com/analytics/devguides/collection/protocol/v1/) and a replacement for Google Analytic's 4 | client-side JavaScript ([analytics.js](https://developers.google.com/analytics/devguides/collection/analyticsjs/)) 5 | to increase privacy and help prevent possible data leakage. 6 | 7 | To minimize implementation changes, GAP attempts to mirror syntax from _analytics.js_. 8 | 9 | The proxy service is intended to be run as an [AWS Lambda](https://aws.amazon.com/lambda/). 10 | 11 | ## Technologies 12 | 13 | - [AWS Lambda](https://aws.amazon.com/lambda/) - The service will serve as an AWS Lambda instance. 14 | - [aws-serverless-express](https://github.com/awslabs/aws-serverless-express) - The server is built with ExpressJS with the npm module specifically for AWS Lambda. 15 | 16 | 17 | ## Installation of Proxy Service 18 | 19 | Clone the repo. Open your terminal and in the folder you just downloaded, run 20 | ```sh 21 | $ npm install 22 | ``` 23 | 24 | ### Start the service 25 | To execute the service locally, run 26 | ```sh 27 | $ npm start 28 | ``` 29 | The server will be executed on _localhost:3001_. 30 | 31 | ## Usage 32 | 33 | To use GAP on a web page, load the client-side JavaScript from the Proxy Service: 34 | 35 | 36 | ```javascript 37 | 46 | ``` 47 | 48 | The code should be added near the top of the `` tag with the string `'UA-XXXXX-Y'`. 49 | 50 | - Replace _UA-XXXXX-Y_ of the Google Analytics property you wish to track. 51 | - Replace _xxx.org_ with the location of your GAP client-side JavaScript. 52 | - Optionally, replace _auto_ with a cookie fields object (see below). 53 | 54 | Adding this code will load GAP and track the current pageview. 55 | 56 | ### Specifying Cookie Fields 57 | 58 | To optionally control how the client-side cookie is set, pass a cookie fields object: 59 | 60 | ```javascript 61 | gap('create', 'UA-XXXXX-Y', { 62 | clientId: '76c24efd-ec42-492a-92df-c62cfd4540a3', 63 | cookieDomain: 'example.org', 64 | cookieExpires: 60 * 60 * 24 // Time in seconds (1 day) 65 | }); 66 | ``` 67 | 68 | ### Initializing GAP Parameters 69 | 70 | You can optionally specify GAP parameters: 71 | 72 | ```javascript 73 | gap('init', 'MetricNameSpace'); 74 | ``` 75 | 76 | - Replace _MetricNameSpace_ with the namespace to be used for metrics. 77 | - Replace _123456-abcde-123456-abcde_ with a Client ID. If a Client ID is not specified, it will 78 | be randomly generated. 79 | 80 | 81 | 82 | ### Tracking Page Views 83 | 84 | Pageview hits can be sent using the send command and specifying a hitType of pageview. The send command has the following signature for the pageview hit type: 85 | 86 | ```javascript 87 | gap('send', 'pageview', [page], [fieldsObject]); 88 | ``` 89 | 90 | Documentation for additional pageview parameters is available from the 91 | [analytics.js documentation](https://developers.google.com/analytics/devguides/collection/analyticsjs/pages). 92 | 93 | ### Tracking Events 94 | 95 | Event hits can be sent using the send command and specifying a hitType of event. The send command has the following signature for the event hit type: 96 | 97 | ```javascript 98 | gap('send', 'event', [eventCategory], [eventAction], [eventLabel], [eventValue], [fieldsObject]); 99 | ``` 100 | Documentation for additional event parameters is available from the 101 | [analytics.js documentation](https://developers.google.com/analytics/devguides/collection/analyticsjs/events). 102 | 103 | 104 | ## Deployment 105 | 106 | To deploy the service as an AWS Lambda instance, in the root of the folder, create a file named ".env". Follow the format below, 107 | 108 | ```sh 109 | AWS_ENVIRONMENT=[the environment you want] 110 | AWS_ACCESS_KEY_ID=[your access key] 111 | AWS_SECRET_ACCESS_KEY=[your access key secret] 112 | AWS_PROFILE= 113 | AWS_SESSION_TOKEN= 114 | AWS_ROLE_ARN=[your lambda instance role] 115 | AWS_REGION=[your lambda region] 116 | AWS_FUNCTION_NAME= 117 | AWS_HANDLER=index.handler 118 | AWS_MEMORY_SIZE=128 119 | AWS_TIMEOUT=3 120 | AWS_DESCRIPTION= 121 | AWS_RUNTIME=nodejs4.3 122 | AWS_VPC_SUBNETS= 123 | AWS_VPC_SECURITY_GROUPS= 124 | EXCLUDE_GLOBS="event.json" 125 | PACKAGE_DIRECTORY=build 126 | ``` 127 | 128 | To get your AWS Lambda service credentials, please visit [AWS Lambda's website](https://aws.amazon.com/lambda/). 129 | 130 | Then run: 131 | 132 | ``` 133 | node-lambda deploy --functionName googleAnalyticsProxy --environment development --configFile deploy.env 134 | ``` 135 | 136 | It will deploy your server as a Lambda instance to your AWS account. -------------------------------------------------------------------------------- /api/controllers/create.js: -------------------------------------------------------------------------------- 1 | const cloudwatch = require('./../helpers/cloudwatch') 2 | const gaConfig = require('./../../config/ga_config.js'); 3 | 4 | /** 5 | * create(req, res) 6 | * 7 | * @param {req} HTTP request 8 | * @param {res} HTTP response 9 | */ 10 | function create(req, res) { 11 | res 12 | .status(200) 13 | .header('Access-Control-Allow-Origin', '*') 14 | .header('Access-Control-Allow-Headers', 'Content-Type') 15 | .header('Cache-Control', 'no-cache, no-store, must-revalidate') 16 | .sendFile(gaConfig.pixelPath); 17 | 18 | if (req.query.firstVisit) { 19 | gaConfig.logger.info(req.query, 'Successfully tracked first visit'); 20 | 21 | cloudwatch.recordMetric(req, gaConfig.visitMetricName); 22 | } 23 | } 24 | 25 | module.exports = { 26 | create: create 27 | }; 28 | -------------------------------------------------------------------------------- /api/controllers/javascript.js: -------------------------------------------------------------------------------- 1 | /** 2 | * getJavascript(req, res) 3 | * 4 | * @param {req} HTTP request 5 | * @param {res} HTTP response 6 | */ 7 | function getJavascript(req, res) { 8 | res 9 | .status(200) 10 | .header('Access-Control-Allow-Origin', '*') 11 | .header('Access-Control-Allow-Headers', 'Content-Type') 12 | .header('Content-Type', 'application/javascript') 13 | .header('Cache-Control', 'max-age=600') 14 | .render('gap', { 15 | serviceBaseUrl: process.env.SERVICE_BASE_URL 16 | }); 17 | } 18 | 19 | module.exports = { 20 | getJavascript: getJavascript 21 | }; 22 | -------------------------------------------------------------------------------- /api/controllers/send.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const axios = require('axios'); 4 | const gaConfig = require('./../../config/ga_config.js'); 5 | const querystring = require('querystring'); 6 | const cloudwatch = require('./../helpers/cloudwatch') 7 | 8 | /** 9 | * checkRequiredParameters(req) 10 | * 11 | * @param {req} HTTP request 12 | */ 13 | function checkMinimumParameters (req) { 14 | if (!req.query.trackingId) throw new Error('Tracking ID was not specified.') 15 | if (!req.query.clientId) throw new Error('Client ID was not specified.') 16 | } 17 | 18 | /** 19 | * sendPageView(req, res) 20 | * 21 | * @param {req} HTTP request 22 | * @param {res} HTTP response 23 | */ 24 | function sendPageView (req, res) { 25 | checkMinimumParameters(req); 26 | 27 | if (!req.query.page) throw new Error('Page was not specified.') 28 | 29 | let payLoad = { 30 | v: 1, 31 | tid: req.query.trackingId, 32 | cid: req.query.clientId, 33 | t: 'pageview', 34 | dh: req.query.host, 35 | dp: req.query.page 36 | }; 37 | 38 | axios({ 39 | method: 'post', 40 | url: gaConfig.googleBaseUrl + '/collect', 41 | data: querystring.stringify(payLoad), 42 | headers: {} 43 | }) 44 | .then(response => { 45 | gaConfig.logger.info(payLoad, 'Successfully tracked page view'); 46 | }) 47 | .catch(response => { 48 | gaConfig.logger.error(payLoad, response, 'Error tracking pageview'); 49 | }); 50 | 51 | res 52 | .status(200) 53 | .header('Cache-Control', 'no-cache, no-store, must-revalidate') 54 | .header('Access-Control-Allow-Origin', '*') 55 | .header('Access-Control-Allow-Headers', 'Content-Type') 56 | .sendFile(gaConfig.pixelPath); 57 | 58 | cloudwatch.recordMetric(req, gaConfig.pageViewMetricName); 59 | } 60 | 61 | /** 62 | * sendEvent(req, res) 63 | * 64 | * @param {req} HTTP request 65 | * @param {res} HTTP response 66 | */ 67 | function sendEvent (req, res) { 68 | checkMinimumParameters(req); 69 | 70 | if (!req.query.eventCategory) throw new Error('Event category was not specified.') 71 | if (!req.query.eventAction) throw new Error('Event action was not specified.') 72 | 73 | let payLoad = { 74 | v: 1, 75 | tid: req.query.trackingId, 76 | cid: req.query.clientId, 77 | t: 'event', 78 | ec: req.query.eventCategory, 79 | ea: req.query.eventAction 80 | }; 81 | 82 | if (req.query.eventLabel) payLoad.el = req.query.eventLabel 83 | if (req.query.eventValue) payLoad.ev = req.query.eventValue 84 | 85 | axios({ 86 | method: 'post', 87 | url: gaConfig.googleBaseUrl + '/collect', 88 | data: querystring.stringify(payLoad), 89 | headers: {} 90 | }) 91 | .then(response => { 92 | gaConfig.logger.info(payLoad, 'Successfully tracked event'); 93 | }) 94 | .catch(response => { 95 | gaConfig.logger.error(payLoad, response, 'Error tracking event'); 96 | }); 97 | 98 | res 99 | .status(200) 100 | .header('Cache-Control', 'no-cache, no-store, must-revalidate') 101 | .header('Access-Control-Allow-Origin', '*') 102 | .header('Access-Control-Allow-Headers', 'Content-Type') 103 | .sendFile(gaConfig.pixelPath); 104 | 105 | cloudwatch.recordMetric(req, gaConfig.pageViewMetricName); 106 | } 107 | 108 | module.exports = { 109 | sendPageView: sendPageView, 110 | sendEvent: sendEvent 111 | }; 112 | -------------------------------------------------------------------------------- /api/helpers/cloudwatch.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const cloudwatch = new AWS.CloudWatch(); 3 | const gaConfig = require('./../../config/ga_config.js'); 4 | 5 | function recordMetric(req, metricName) { 6 | if (!req.query.metricNameSpace) { 7 | return false; 8 | } 9 | 10 | const params = { 11 | MetricData: [ 12 | { 13 | MetricName: metricName, 14 | Value: 1 15 | }, 16 | ], 17 | Namespace: req.query.metricNameSpace 18 | }; 19 | 20 | cloudwatch.putMetricData(params, function(err, data) { 21 | if (err) { 22 | gaConfig.logger.error(err, err.stack); 23 | return false; 24 | } 25 | 26 | gaConfig.logger.info('Recorded CloudWatch metric: ' + req.query.metricNameSpace + ':' + metricName); 27 | return true; 28 | }); 29 | } 30 | 31 | module.exports = { 32 | recordMetric: recordMetric 33 | }; -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const express = require('express') 4 | 5 | require('dotenv').config() 6 | 7 | const minify = require('express-minify') 8 | 9 | const gaConfig = require('./config/ga_config.js'); 10 | const create = require('./api/controllers/create.js') 11 | const send = require('./api/controllers/send.js') 12 | const javascript = require('./api/controllers/javascript.js') 13 | 14 | const app = express() 15 | 16 | app.set('view engine', 'ejs') 17 | app.use(minify()) 18 | 19 | app.options('*', function (req, res) { 20 | res 21 | .status(200) 22 | .header('Access-Control-Allow-Headers', 'Content-Type') 23 | .header('Access-Control-Allow-Origin', '*') 24 | .send() 25 | }) 26 | 27 | let router = express.Router() 28 | app.use(process.env.BASE_PATH, router) 29 | 30 | router.route('/swagger') 31 | .get(function (req, res) { 32 | res 33 | .status(200) 34 | .header('Access-Control-Allow-Headers', 'Content-Type') 35 | .header('Access-Control-Allow-Origin', '*') 36 | .sendFile(__dirname + '/swagger/v0.1.json') 37 | }) 38 | 39 | router.route('/javascript/gaproxy.js') 40 | .get(javascript.getJavascript) 41 | 42 | router.route('/create') 43 | .get(create.create) 44 | 45 | router.route('/send/pageview') 46 | .get(send.sendPageView) 47 | 48 | router.route('/send/event') 49 | .get(send.sendEvent) 50 | 51 | if (!process.env.AWS_LAMBDA_FUNCTION_NAME) { 52 | gaConfig.logger.info('Using ' + process.env.BASE_PATH + ' for base path.') 53 | gaConfig.logger.info('Server listing on port ' + (process.env.PORT || 3001) + ' at ' + process.env.BASE_PATH + '.') 54 | app.listen(process.env.PORT || 3001) 55 | } 56 | 57 | module.exports = app 58 | -------------------------------------------------------------------------------- /config/ga_config.js: -------------------------------------------------------------------------------- 1 | const bunyan = require('bunyan') 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | googleBaseUrl: 'https://www.google-analytics.com', 6 | visitMetricName: 'Visit', 7 | pageViewMetricName: 'PageView', 8 | logger: bunyan.createLogger({ 9 | name: 'App' 10 | }), 11 | pixelPath: path.resolve(__dirname + '/../static/collect.gif') 12 | }; -------------------------------------------------------------------------------- /context.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /deploy.env: -------------------------------------------------------------------------------- 1 | SERVICE_BASE_URL=https://api.nypltech.org/api/v0.1/ga-proxy 2 | BASE_PATH=/api/v0.1/ga-proxy -------------------------------------------------------------------------------- /event.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /event_sources.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const awsServerlessExpress = require('aws-serverless-express'); 4 | const app = require('./app'); 5 | 6 | const binaryTypes = [ 7 | 'image/gif' 8 | ] 9 | 10 | const server = awsServerlessExpress.createServer(app, null, binaryTypes); 11 | 12 | exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-analytics-proxy", 3 | "version": "0.0.1", 4 | "description": "Google Analytics Proxy", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "nodemon app.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/NYPL/google-analytics-proxy" 12 | }, 13 | "keywords": [ 14 | "aws lambda", 15 | "expess", 16 | "postgresql", 17 | "swagger" 18 | ], 19 | "author": "NYPL Digital Team", 20 | "license": "", 21 | "dependencies": { 22 | "aws-sdk": "^2.41.0", 23 | "aws-serverless-express": "^2.1.3", 24 | "axios": "0.15.3", 25 | "dotenv": "^4.0.0", 26 | "ejs": "^1.0.0", 27 | "express": "^4.12.3", 28 | "express-minify": "^0.2.0", 29 | "serverless": "^1.9.0", 30 | "serverless-plugin-write-env-vars": "^1.0.1", 31 | "bunyan": "^1.8.10" 32 | }, 33 | "devDependencies": { 34 | "supertest": "1.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /static/collect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NYPL/google-analytics-proxy/4c131a2897056ffef81aa242805e551f308f08bf/static/collect.gif -------------------------------------------------------------------------------- /swagger/v0.1.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "swagger": "2.0", 4 | "info": { 5 | "version": "0.1", 6 | "title": "Google Analytics Proxy API" 7 | }, 8 | "host": "api.nypltech.org", 9 | "basePath": "/api", 10 | "schemes": [ 11 | "https" 12 | ], 13 | "tags": [ 14 | { 15 | "name": "gaproxy", 16 | "description": "Google Analytics Proxy API" 17 | } 18 | ], 19 | "paths": { 20 | "/v0.1/ga-proxy/javascript/gaproxy.js": { 21 | "get": { 22 | "tags": [ 23 | "gaproxy" 24 | ], 25 | "summary": "Generate a JavaScript tracking snippet (gaproxy.js)", 26 | "description": "See: https://developers.google.com/analytics/devguides/collection/analyticsjs/#the_javascript_tracking_snippet", 27 | "produces": [ 28 | "application/javascript" 29 | ], 30 | "responses": { 31 | "200": { 32 | "description": "JavaScript tracking snippet", 33 | "type": "string" 34 | } 35 | } 36 | } 37 | }, 38 | "/v0.1/ga-proxy/create": { 39 | "get": { 40 | "tags": [ 41 | "gaproxy" 42 | ], 43 | "summary": "Create a Tracker object", 44 | "description": "See: https://developers.google.com/analytics/devguides/collection/analyticsjs/creating-trackers", 45 | "parameters": [ 46 | { 47 | "name": "trackingId", 48 | "in": "query", 49 | "required": true, 50 | "type": "string" 51 | }, 52 | { 53 | "name": "cookieDomain", 54 | "in": "query", 55 | "required": true, 56 | "type": "string" 57 | }, 58 | { 59 | "name": "clientId", 60 | "in": "query", 61 | "required": true, 62 | "type": "string" 63 | }, 64 | { 65 | "name": "metricNameSpace", 66 | "in": "query", 67 | "required": false, 68 | "type": "string" 69 | } 70 | ], 71 | "produces": [ 72 | "image/gif" 73 | ], 74 | "responses": { 75 | "200": { 76 | "description": "A 1x1 pixel in GIF format" 77 | } 78 | } 79 | } 80 | }, 81 | "/v0.1/ga-proxy/send/pageview": { 82 | "get": { 83 | "tags": [ 84 | "gaproxy" 85 | ], 86 | "summary": "Send page tracking information", 87 | "description": "See: https://developers.google.com/analytics/devguides/collection/analyticsjs/pages", 88 | "parameters": [ 89 | { 90 | "name": "trackingId", 91 | "in": "query", 92 | "required": true, 93 | "type": "string" 94 | }, 95 | { 96 | "name": "clientId", 97 | "in": "query", 98 | "required": true, 99 | "type": "string" 100 | }, 101 | { 102 | "name": "metricNameSpace", 103 | "in": "query", 104 | "required": false, 105 | "type": "string" 106 | }, 107 | { 108 | "name": "page", 109 | "in": "query", 110 | "required": true, 111 | "type": "string" 112 | }, 113 | { 114 | "name": "host", 115 | "in": "query", 116 | "required": false, 117 | "type": "string" 118 | } 119 | ], 120 | "produces": [ 121 | "image/gif" 122 | ], 123 | "responses": { 124 | "200": { 125 | "description": "A 1x1 pixel in GIF format" 126 | } 127 | } 128 | } 129 | }, 130 | "/v0.1/ga-proxy/send/event": { 131 | "get": { 132 | "tags": [ 133 | "gaproxy" 134 | ], 135 | "summary": "Send an event", 136 | "description": "See: https://developers.google.com/analytics/devguides/collection/analyticsjs/events", 137 | "parameters": [ 138 | { 139 | "name": "trackingId", 140 | "in": "query", 141 | "required": true, 142 | "type": "string" 143 | }, 144 | { 145 | "name": "clientId", 146 | "in": "query", 147 | "required": true, 148 | "type": "string" 149 | }, 150 | { 151 | "name": "metricNameSpace", 152 | "in": "query", 153 | "required": true, 154 | "type": "string" 155 | }, 156 | { 157 | "name": "metricNameSpace", 158 | "in": "query", 159 | "required": true, 160 | "type": "string" 161 | }, 162 | { 163 | "name": "eventCategory", 164 | "in": "query", 165 | "required": true, 166 | "type": "string" 167 | }, 168 | { 169 | "name": "eventAction", 170 | "in": "query", 171 | "required": true, 172 | "type": "string" 173 | }, 174 | { 175 | "name": "eventLabel", 176 | "in": "query", 177 | "required": false, 178 | "type": "string" 179 | }, 180 | { 181 | "name": "eventValue", 182 | "in": "query", 183 | "required": false, 184 | "type": "string" 185 | } 186 | ], 187 | "produces": [ 188 | "image/gif" 189 | ], 190 | "responses": { 191 | "200": { 192 | "description": "A 1x1 pixel in GIF format" 193 | } 194 | } 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /views/gap.ejs: -------------------------------------------------------------------------------- 1 | var gaProxyApp = { 2 | clientId: '', 3 | metricNameSpace: '', 4 | isFirstVisit: false, 5 | trackingId: '', 6 | clientIdCookieName: 'gap.clientId', 7 | serviceBaseUrl: '<%= serviceBaseUrl %>', 8 | 9 | collect: function (action, data) { 10 | data.trackingId = this.trackingId; 11 | data.clientId = this.clientId; 12 | data.metricNameSpace = this.metricNameSpace; 13 | data.c = new Date().getTime(); 14 | 15 | this.img = document.createElement('img'); 16 | this.img.src = this.serviceBaseUrl + '/' + action.join('/') + '?' + this.serialize(data); 17 | }, 18 | 19 | serialize: function (obj) { 20 | var str = []; 21 | for (var p in obj) 22 | if (obj.hasOwnProperty(p) && obj[p]) { 23 | str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p])); 24 | } 25 | return str.join('&'); 26 | }, 27 | 28 | processQueue: function (queue) { 29 | while (queue.length) { 30 | try { 31 | this.processQueueElement(queue.shift()); 32 | } catch (error) { 33 | console.error(error); 34 | } 35 | } 36 | }, 37 | 38 | processQueueElement: function (element) { 39 | var action = element[0]; 40 | 41 | switch (action) { 42 | case 'init': 43 | this.init(element); 44 | break; 45 | 46 | case 'create': 47 | this.collect([action], this.create(element)); 48 | break; 49 | 50 | case 'send': 51 | this.collect([action, element[1]], this.send(element)); 52 | break; 53 | 54 | default: 55 | throw('Action specified (' + action + ') is not valid.'); 56 | break; 57 | } 58 | }, 59 | 60 | setCookie: function (name, value, secondsExpire) { 61 | var expires = ''; 62 | 63 | if (secondsExpire) { 64 | var date = new Date(); 65 | date.setTime(date.getTime() + (secondsExpire * 1000)); 66 | expires = '; expires=' + date.toUTCString(); 67 | } 68 | 69 | document.cookie = name + '=' + value + expires + '; path=/'; 70 | }, 71 | 72 | readCookie: function (name) { 73 | var nameEQ = name + '='; 74 | var ca = document.cookie.split(';'); 75 | for (var i = 0; i < ca.length; i++) { 76 | var c = ca[i]; 77 | while (c.charAt(0) == ' ') c = c.substring(1, c.length); 78 | if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); 79 | } 80 | return null; 81 | }, 82 | 83 | createGuid: function () { 84 | function s4 () { 85 | return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); 86 | } 87 | 88 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); 89 | }, 90 | 91 | checkRequired: function () { 92 | if (!this.trackingId) { 93 | throw('Tracking ID was not set.'); 94 | } 95 | 96 | return true; 97 | }, 98 | 99 | inferFirstVisit: function () { 100 | return !this.readCookie(this.clientIdCookieName); 101 | }, 102 | 103 | inferClientId: function (cookieFields) { 104 | if (cookieFields.clientId) { 105 | return cookieFields.clientId; 106 | } 107 | 108 | if (this.readCookie(this.clientIdCookieName)) { 109 | return this.readCookie(this.clientIdCookieName); 110 | } 111 | 112 | return this.generateClientId(); 113 | }, 114 | 115 | inferSecondsExpire: function (cookieFields) { 116 | if (typeof cookieFields === 'object') { 117 | return cookieFields.cookieExpires; 118 | } 119 | }, 120 | 121 | generateClientId: function () { 122 | return this.createGuid(); 123 | }, 124 | 125 | init: function (element) { 126 | this.metricNameSpace = element[1]; 127 | }, 128 | 129 | create: function (element) { 130 | if (!element[1]) { 131 | throw('Tracking ID was not specified'); 132 | } 133 | 134 | this.trackingId = element[1]; 135 | 136 | this.clientId = this.inferClientId(element[2]); 137 | 138 | this.isFirstVisit = this.inferFirstVisit(); 139 | 140 | if (this.isFirstVisit) { 141 | this.setCookie( 142 | this.clientIdCookieName, 143 | this.clientId, 144 | this.inferSecondsExpire(element[2]) 145 | ); 146 | } 147 | 148 | return { 149 | trackingId: element[1], 150 | firstVisit: this.isFirstVisit 151 | }; 152 | }, 153 | 154 | send: function (element) { 155 | this.checkRequired(); 156 | 157 | var sender = { 158 | pageView: function (element) { 159 | return { 160 | host: window.location.hostname, 161 | page: element[2] ? element[2] : document.location.pathname + document.location.hash, 162 | fieldsObject: element[3] 163 | }; 164 | }, 165 | 166 | event: function (element) { 167 | if (!element[2].length) { 168 | throw('Event category was not specified.') 169 | } 170 | 171 | if (!element[3].length) { 172 | throw('Event action was not specified.') 173 | } 174 | 175 | return { 176 | eventCategory: element[2], 177 | eventAction: element[3], 178 | eventLabel: element[4], 179 | eventValue: element[5], 180 | fieldsObject: element[6] 181 | }; 182 | } 183 | } 184 | 185 | var eventCategory = element[1]; 186 | 187 | switch (eventCategory) { 188 | case 'pageview': 189 | return sender.pageView(element); 190 | break; 191 | 192 | case 'event': 193 | return sender.event(element); 194 | break; 195 | 196 | default: 197 | throw('eventCategory specified (' + eventCategory + ') is not valid.'); 198 | break; 199 | } 200 | } 201 | }; 202 | 203 | (function () { 204 | // Check if GAProxy object is set 205 | if (!window.gap) { 206 | console.error('GA proxy object is not defined.'); 207 | return false; 208 | } 209 | 210 | // Add queue processing functionality 211 | window.gap.q.push = function () { 212 | Array.prototype.push.apply(this, arguments); 213 | gaProxyApp.processQueue(window.gap.q); 214 | } 215 | 216 | var lastDocumentUrl = ''; 217 | 218 | // Track changes in browser history 219 | window.onpopstate = function (event) { 220 | if (lastDocumentUrl !== document.location.href) { 221 | window.gap('send', 'pageview'); 222 | lastDocumentUrl = document.location.href; 223 | } 224 | }; 225 | 226 | // Process initial queue events 227 | gaProxyApp.processQueue(window.gap.q); 228 | })(); 229 | --------------------------------------------------------------------------------