├── .gitignore ├── test ├── 01_ApiHelper.js ├── 02_Client.js └── 03_DimensionFilter.js ├── src ├── ResultParser │ └── index.js ├── Client │ └── index.js ├── MetricFilter │ └── index.js ├── DimensionFilter │ └── index.js ├── ObjectBuilder │ └── index.js ├── ApiHelper │ └── index.js └── Request │ └── index.js ├── examples └── example_01.js ├── LICENSE ├── package.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | usage/ 3 | .npm 4 | .env 5 | .nyc_output 6 | key.json -------------------------------------------------------------------------------- /test/01_ApiHelper.js: -------------------------------------------------------------------------------- 1 | const should = require("chai").should(); 2 | 3 | const ApiHelper = require("../src/ApiHelper"); 4 | 5 | function randomString() { 6 | return Math.random() 7 | .toString(36) 8 | .substr(2, 5); 9 | } 10 | 11 | describe("ApiHelper", function() { 12 | var str = randomString(); 13 | it("should prepend '" + str + "' with ga:", function(done) { 14 | ApiHelper.generateApiName(str).should.equal("ga:" + str); 15 | done(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/ResultParser/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | cleanKeys: function(columns) { 3 | return columns.map(function(column) { 4 | return column.replace(/^ga:/, ""); 5 | }); 6 | }, 7 | castValue: function(value) { 8 | var newValue = Number(value); 9 | 10 | if (isNaN(newValue)) { 11 | return value; 12 | } 13 | 14 | return newValue; 15 | }, 16 | mergeKeyValueArrays: function(keys, values) { 17 | var that = this; 18 | return Object.assign.apply( 19 | {}, 20 | keys.map((v, i) => ({ 21 | [v]: that.castValue(values[i]) 22 | })) 23 | ); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/Client/index.js: -------------------------------------------------------------------------------- 1 | var { google } = require("googleapis"); 2 | var fs = require("fs"); 3 | 4 | module.exports = { 5 | createFromKeyFile: function(path) { 6 | return this.createFromKey(JSON.parse(fs.readFileSync(path, "utf8"))); 7 | }, 8 | createFromKey: function(key) { 9 | var params = { 10 | email: key.client_email, 11 | privateKey: key.private_key, 12 | permissions: ["https://www.googleapis.com/auth/analytics.readonly"] 13 | }; 14 | return this.createFromParams(params); 15 | }, 16 | createFromParams: function(params) { 17 | return new google.auth.JWT(params.email, null, params.privateKey, params.permissions, null); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/MetricFilter/index.js: -------------------------------------------------------------------------------- 1 | var ObjectBuilder = require("../ObjectBuilder"); 2 | var ApiHelper = require("../ApiHelper"); 3 | 4 | var MetricFilter = function() {}; 5 | 6 | MetricFilter.prototype = Object.create(new ObjectBuilder()); 7 | 8 | MetricFilter.prototype.metric = function(name) { 9 | name = ApiHelper.generateApiName(name); 10 | return this.set("metricName", name); 11 | }; 12 | 13 | MetricFilter.prototype.condition = function(operator = "EQUAL", value) { 14 | this.set("operator", operator); 15 | return this.set("comparisonValue", value.toString()); 16 | }; 17 | 18 | MetricFilter.prototype.not = function() { 19 | return this.set("not", true); 20 | }; 21 | 22 | module.exports = MetricFilter; -------------------------------------------------------------------------------- /src/DimensionFilter/index.js: -------------------------------------------------------------------------------- 1 | // https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet 2 | 3 | var ObjectBuilder = require("../ObjectBuilder"); 4 | var ApiHelper = require("../ApiHelper"); 5 | 6 | var DimensionFilter = function() {}; 7 | 8 | DimensionFilter.prototype = Object.create(new ObjectBuilder()); 9 | 10 | DimensionFilter.prototype.condition = function(operator = "EXACT", value) { 11 | this.set("operator", operator); 12 | return this.set("expressions", value); 13 | }; 14 | 15 | DimensionFilter.prototype.dimension = function(name) { 16 | name = ApiHelper.generateApiName(name); 17 | return this.set("dimensionName", name); 18 | }; 19 | 20 | DimensionFilter.prototype.not = function() { 21 | return this.set("not", true); 22 | }; 23 | 24 | module.exports = DimensionFilter; -------------------------------------------------------------------------------- /test/02_Client.js: -------------------------------------------------------------------------------- 1 | const should = require("chai").should(); 2 | const fs = require("fs"); 3 | 4 | const Client = require("../src/Client"); 5 | 6 | describe("Client", function() { 7 | it("should create a client from key file", function(done) { 8 | var cl = Client.createFromKeyFile("./key.json"); 9 | cl.constructor.name.should.equal("JWT"); 10 | done(); 11 | }); 12 | var key = JSON.parse(fs.readFileSync("./key.json", "utf8")); 13 | it("should create a client from key", function(done) { 14 | var cl = Client.createFromKey(key); 15 | cl.constructor.name.should.equal("JWT"); 16 | done(); 17 | }); 18 | it("should create a client from params", function(done) { 19 | var cl = Client.createFromParams({ 20 | email: key.client_email, 21 | privateKey: key.private_key, 22 | permissions: ["https://www.googleapis.com/auth/analytics.readonly"] 23 | }); 24 | cl.constructor.name.should.equal("JWT"); 25 | done(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/example_01.js: -------------------------------------------------------------------------------- 1 | require("dotenv").load(); 2 | 3 | const path = require("path"); 4 | const { SimpleGA, Request } = require("../index.js"); 5 | 6 | (async function() { 7 | var analytics = new SimpleGA(path.join(__dirname, "../key.json")); 8 | 9 | var request1 = Request() 10 | .select("dimension12","pageviews","users") 11 | .from(process.env.GA_VIEW_ID) 12 | .during("2018-09-20","2018-09-25") 13 | .orderDesc("pageviews") 14 | .limit(20); 15 | 16 | var request2 = Request() 17 | .select("dimension12","entrances") 18 | .from(process.env.GA_VIEW_ID) 19 | .during("2018-09-20","2018-09-25") 20 | .orderDesc("entrances") 21 | .limit(20); 22 | 23 | try { 24 | 25 | var r1 = analytics.run(request1,{ 26 | rename: { 27 | dimension12: "title" 28 | } 29 | }); 30 | var r2 = analytics.run(request2,{ 31 | rename: { 32 | dimension12: "title" 33 | } 34 | }); 35 | 36 | var [res1,res2] = await Promise.all([ 37 | r1,r2 38 | ]); 39 | 40 | console.log({res1,res2}); 41 | } catch (err) { 42 | console.error(err); 43 | } 44 | })(); 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ringoldslescinskis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-simple-ga", 3 | "version": "0.5.1", 4 | "description": "A simple to use NodeJs package for the Google Analytics Reporting API", 5 | "keywords": [ 6 | "nodejs", 7 | "google analytics", 8 | "ga", 9 | "analytics api", 10 | "google analytics api", 11 | "analytics reporting api v4", 12 | "universal analytics", 13 | "analytics v4", 14 | "reporting api v4", 15 | "google analytics client" 16 | ], 17 | "main": "index.js", 18 | "scripts": { 19 | "test": "node node_modules/nyc/bin/nyc node_modules/mocha/bin/mocha", 20 | "prettier": "prettier --no-config --print-width 120 --parser flow --use-tabs --write *.js **/*.js" 21 | }, 22 | "homepage": "https://github.com/lescinskiscom/node-simple-ga#readme", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/lescinskiscom/node-simple-ga" 26 | }, 27 | "author": { 28 | "name": "Ringolds Lescinskis", 29 | "email": "ringolds@lescinskis.com", 30 | "url": "https://www.lescinskis.com" 31 | }, 32 | "license": "MIT", 33 | "dependencies": { 34 | "clone-deep": "^4.0.0", 35 | "googleapis": "^66.0.0", 36 | "moment": "^2.22.2" 37 | }, 38 | "devDependencies": { 39 | "chai": "^4.1.2", 40 | "dotenv": "^6.0.0", 41 | "mocha": "^8.2.1", 42 | "nyc": "^15.1.0", 43 | "prettier": "^1.13.7" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ObjectBuilder/index.js: -------------------------------------------------------------------------------- 1 | const cloneDeep = require("clone-deep"); 2 | 3 | var ObjectBuilder = function() { 4 | this.data = {}; 5 | }; 6 | 7 | ObjectBuilder.prototype.reset = function() { 8 | this.data = {}; 9 | return this; 10 | }; 11 | 12 | ObjectBuilder.prototype.set = function(key, value) { 13 | if (value == null) { 14 | return this.clear(key); 15 | } 16 | 17 | var obj = {}; 18 | var selectedObj = obj; 19 | 20 | key.split(".").forEach(function(fragment, i, array) { 21 | selectedObj[fragment] = {}; 22 | if (i === array.length - 1) { 23 | selectedObj[fragment] = value; 24 | } 25 | selectedObj = selectedObj[fragment]; 26 | }); 27 | 28 | this.data = { ...this.data, ...obj }; 29 | return this; 30 | }; 31 | 32 | ObjectBuilder.prototype.get = function(key) { 33 | return this.data[key] ? this.data[key] : null; 34 | }; 35 | 36 | ObjectBuilder.prototype.append = function(key, ...values) { 37 | if (!this.data[key]) { 38 | this.data[key] = []; 39 | } 40 | values = this.getValues(values); 41 | 42 | values.forEach( 43 | function(value) { 44 | this.data[key].push(value); 45 | }.bind(this) 46 | ); 47 | 48 | return this; 49 | }; 50 | 51 | ObjectBuilder.prototype.getValues = function(values) { 52 | if (!values) { 53 | return []; 54 | } 55 | if (values.length === 0) { 56 | return []; 57 | } 58 | 59 | if (values.length === 1 && Array.isArray(values[0])) { 60 | values = values[0]; 61 | } 62 | 63 | return values; 64 | }; 65 | 66 | ObjectBuilder.prototype.remove = function(key, param, ...values) { 67 | if (!this.data[key]) { 68 | return this; 69 | } 70 | 71 | this.getValues(values).forEach( 72 | function(value) { 73 | this.data[key] = this.data[key].filter(function(entry) { 74 | return entry[param] !== value; 75 | }); 76 | }.bind(this) 77 | ); 78 | 79 | if (this.data.length == 0) { 80 | this.clear(key); 81 | } 82 | 83 | return this; 84 | }; 85 | 86 | ObjectBuilder.prototype.clear = function(key) { 87 | if (!key) { 88 | return this.reset(); 89 | } 90 | delete this.data[key]; 91 | return this; 92 | }; 93 | 94 | ObjectBuilder.prototype.make = function() { 95 | return JSON.parse(JSON.stringify(this.data)); 96 | }; 97 | 98 | ObjectBuilder.prototype.clone = function(type, filter) { 99 | return cloneDeep(this); 100 | }; 101 | 102 | module.exports = ObjectBuilder; 103 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // https://developers.google.com/adsense/management/reporting/relative_dates 2 | 3 | const Client = require("./src/Client"); 4 | const Request = require("./src/Request"); 5 | const DimensionFilter = require("./src/DimensionFilter"); 6 | const MetricFilter = require("./src/MetricFilter"); 7 | const ResultParser = require("./src/ResultParser"); 8 | 9 | const { google } = require("googleapis"); 10 | const cloneDeep = require("clone-deep"); 11 | 12 | const SimpleGA = function(param) { 13 | this.client = null; 14 | 15 | if (typeof param === "string") { 16 | this.client = Client.createFromKeyFile(param); 17 | } else { 18 | if (param.key) { 19 | this.client = Client.createFromKey(param.key); 20 | } 21 | if (param.keyFile) { 22 | this.client = Client.createFromKeyFile(param.keyFile); 23 | } 24 | } 25 | 26 | if (!this.client) { 27 | throw Error("Google Analytics client wasn't created!"); 28 | } 29 | 30 | this.client.authorize(function(err, tokens) { 31 | if (err) { 32 | throw new Error(err); 33 | } 34 | }); 35 | 36 | this.analytics = google.analyticsreporting("v4"); 37 | }; 38 | 39 | SimpleGA.prototype.runRaw = function(request, params = {}, currentPage = 1) { 40 | 41 | var that = this; 42 | return new Promise(function(resolve, reject) { 43 | var entries = []; 44 | var headers = null; 45 | 46 | that.analytics.reports.batchGet( 47 | { 48 | auth: that.client, 49 | resource: { 50 | reportRequests: [request.make()] 51 | } 52 | }, 53 | async function(err, response) { 54 | if (err) { 55 | return reject(err); 56 | } 57 | 58 | 59 | var report = response.data.reports[0]; 60 | 61 | if (currentPage == 1) { 62 | var metricColumnHeader = report.columnHeader.metricHeader.metricHeaderEntries.map(function(entry) { 63 | return entry.name; 64 | }); 65 | 66 | headers = { 67 | dimensions: ResultParser.cleanKeys(report.columnHeader.dimensions), 68 | metrics: ResultParser.cleanKeys(metricColumnHeader) 69 | }; 70 | } 71 | 72 | if(!("rows" in report.data)) { 73 | return resolve({ headers, entries }); 74 | } 75 | 76 | report.data.rows.forEach(function(entry) { 77 | entries.push({ 78 | dimensions: entry.dimensions, 79 | metrics: entry.metrics[0].values 80 | }); 81 | }); 82 | 83 | // If we're at the last page, stop 84 | if (params.pages && currentPage === params.pages) { 85 | return resolve({ headers, entries }); 86 | } 87 | 88 | // If there are more pages, get the results 89 | if (report.nextPageToken) { 90 | request.pageToken(report.nextPageToken); 91 | var requestedData = await that.runRaw( 92 | request, 93 | { 94 | pages: params.pages 95 | }, 96 | currentPage + 1 97 | ); 98 | entries = entries.concat(requestedData.entries); 99 | } 100 | 101 | return resolve({ headers, entries }); 102 | } 103 | ); 104 | }); 105 | }; 106 | 107 | SimpleGA.prototype.run = function(request, params = {}) { 108 | 109 | var that = this; 110 | 111 | return new Promise(async function(resolve, reject){ 112 | if(request.get("pageSize") && !params.pages) { 113 | params.pages = 1; 114 | } 115 | 116 | var result = await that.runRaw(request, params); 117 | 118 | if (params.rawResults) { 119 | return result; 120 | } 121 | 122 | var processedResult = []; 123 | 124 | result.entries.forEach(function(entry) { 125 | let dimensions = ResultParser.mergeKeyValueArrays(result.headers.dimensions, entry.dimensions); 126 | let metrics = ResultParser.mergeKeyValueArrays(result.headers.metrics, entry.metrics); 127 | processedResult.push(Object.assign({},dimensions,metrics)); 128 | }); 129 | 130 | if(params.rename && Object.keys(params.rename).length > 0 && processedResult.length > 0) { 131 | 132 | let oldKeys = Object.keys(params.rename); 133 | let entry = processedResult[0]; 134 | 135 | oldKeys = oldKeys.filter(function(oldKey){ 136 | return oldKey in entry; 137 | }); 138 | 139 | processedResult = processedResult.map(function(entry){ 140 | oldKeys.forEach(function(oldKey){ 141 | let newKey = params.rename[oldKey]; 142 | entry[newKey] = entry[oldKey]; 143 | delete entry[oldKey]; 144 | }); 145 | return entry; 146 | }); 147 | } 148 | 149 | resolve(processedResult); 150 | 151 | }); 152 | 153 | }; 154 | 155 | module.exports = { 156 | SimpleGA, 157 | Request, 158 | DimensionFilter, 159 | MetricFilter 160 | }; 161 | -------------------------------------------------------------------------------- /test/03_DimensionFilter.js: -------------------------------------------------------------------------------- 1 | const should = require("chai").should(); 2 | const fs = require("fs"); 3 | 4 | const DimensionFilter = require("../src/DimensionFilter"); 5 | 6 | function randomString() { 7 | return Math.random() 8 | .toString(36) 9 | .substr(2, 5); 10 | } 11 | 12 | function randomNumber() { 13 | return Math.floor(Math.random() * 10000); 14 | } 15 | 16 | describe("DimensionFilter", function() { 17 | var str = randomString(); 18 | it("should have operator equal to " + str, function(done) { 19 | var filter = new DimensionFilter().condition("", str).make(); 20 | filter.operator.should.equal(str); 21 | done(); 22 | }); 23 | var str = randomString(); 24 | it("should have expressions equal to ['" + str + "']", function(done) { 25 | var filter = new DimensionFilter().condition(str, "").make(); 26 | filter.expressions.should.eql([str]); 27 | done(); 28 | }); 29 | var str = randomString(); 30 | it("should have dimensionName equal to ga:" + str, function(done) { 31 | var filter = new DimensionFilter().dimension(str).make(); 32 | filter.dimensionName.should.equal("ga:" + str); 33 | done(); 34 | }); 35 | it("should have not equal to true", function(done) { 36 | var filter = new DimensionFilter().not().make(); 37 | filter.not.should.equal(true); 38 | done(); 39 | }); 40 | it("should have not equal to true", function(done) { 41 | var filter = new DimensionFilter().inverse().make(); 42 | filter.not.should.equal(true); 43 | done(); 44 | }); 45 | describe("operator", function() { 46 | it("should have operator equal to REGEXP", function(done) { 47 | var filter = new DimensionFilter().matchRegex("").make(); 48 | filter.operator.should.equal("REGEXP"); 49 | done(); 50 | }); 51 | it("should have operator equal to BEGINS_WITH", function(done) { 52 | var filter = new DimensionFilter().beginsWith("").make(); 53 | filter.operator.should.equal("BEGINS_WITH"); 54 | done(); 55 | }); 56 | it("should have operator equal to ENDS_WITH", function(done) { 57 | var filter = new DimensionFilter().endsWith("").make(); 58 | filter.operator.should.equal("ENDS_WITH"); 59 | done(); 60 | }); 61 | it("should have operator equal to PARTIAL", function(done) { 62 | var filter = new DimensionFilter().contains("").make(); 63 | filter.operator.should.equal("PARTIAL"); 64 | done(); 65 | }); 66 | it("should have operator equal to EXACT", function(done) { 67 | var filter = new DimensionFilter().is("").make(); 68 | filter.operator.should.equal("EXACT"); 69 | done(); 70 | }); 71 | it("should have operator equal to EXACT", function(done) { 72 | var filter = new DimensionFilter().matches("").make(); 73 | filter.operator.should.equal("EXACT"); 74 | done(); 75 | }); 76 | it("should have operator equal to NUMERIC_EQUAL", function(done) { 77 | var filter = new DimensionFilter().equalsTo("").make(); 78 | filter.operator.should.equal("NUMERIC_EQUAL"); 79 | done(); 80 | }); 81 | it("should have operator equal to NUMERIC_EQUAL", function(done) { 82 | var filter = new DimensionFilter().eq("").make(); 83 | filter.operator.should.equal("NUMERIC_EQUAL"); 84 | done(); 85 | }); 86 | it("should have operator equal to NUMERIC_GREATER_THAN", function(done) { 87 | var filter = new DimensionFilter().greaterThan("").make(); 88 | filter.operator.should.equal("NUMERIC_GREATER_THAN"); 89 | done(); 90 | }); 91 | it("should have operator equal to NUMERIC_GREATER_THAN", function(done) { 92 | var filter = new DimensionFilter().gt("").make(); 93 | filter.operator.should.equal("NUMERIC_GREATER_THAN"); 94 | done(); 95 | }); 96 | it("should have operator equal to NUMERIC_LESS_THAN", function(done) { 97 | var filter = new DimensionFilter().lessThan("").make(); 98 | filter.operator.should.equal("NUMERIC_LESS_THAN"); 99 | done(); 100 | }); 101 | it("should have operator equal to NUMERIC_LESS_THAN", function(done) { 102 | var filter = new DimensionFilter().lt("").make(); 103 | filter.operator.should.equal("NUMERIC_LESS_THAN"); 104 | done(); 105 | }); 106 | it("should have operator equal to IN_LIST", function(done) { 107 | var filter = new DimensionFilter().inList("").make(); 108 | filter.operator.should.equal("IN_LIST"); 109 | done(); 110 | }); 111 | }); 112 | 113 | describe("expressions", function() { 114 | var str = randomString(); 115 | it("should equal to ['" + str + "']", function(done) { 116 | var filter = new DimensionFilter().matchRegex(str).make(); 117 | filter.expressions.should.eql([str]); 118 | done(); 119 | }); 120 | it("should equal to ['" + str + "']", function(done) { 121 | var filter = new DimensionFilter().beginsWith(str).make(); 122 | filter.expressions.should.eql([str]); 123 | done(); 124 | }); 125 | it("should equal to ['" + str + "']", function(done) { 126 | var filter = new DimensionFilter().endsWith(str).make(); 127 | filter.expressions.should.eql([str]); 128 | done(); 129 | }); 130 | it("should equal to ['" + str + "']", function(done) { 131 | var filter = new DimensionFilter().contains(str).make(); 132 | filter.expressions.should.eql([str]); 133 | done(); 134 | }); 135 | it("should equal to ['" + str + "']", function(done) { 136 | var filter = new DimensionFilter().is(str).make(); 137 | filter.expressions.should.eql([str]); 138 | done(); 139 | }); 140 | it("should equal to ['" + str + "']", function(done) { 141 | var filter = new DimensionFilter().matches(str).make(); 142 | filter.expressions.should.eql([str]); 143 | done(); 144 | }); 145 | var num = randomNumber(); 146 | it("should equal to ['" + num + "']", function(done) { 147 | var filter = new DimensionFilter().equalsTo(num).make(); 148 | filter.expressions.should.eql([num.toString()]); 149 | done(); 150 | }); 151 | it("should equal to ['" + num + "']", function(done) { 152 | var filter = new DimensionFilter().eq(num).make(); 153 | filter.expressions.should.eql([num.toString()]); 154 | done(); 155 | }); 156 | it("should equal to ['" + num + "']", function(done) { 157 | var filter = new DimensionFilter().greaterThan(num).make(); 158 | filter.expressions.should.eql([num.toString()]); 159 | done(); 160 | }); 161 | it("should equal to ['" + num + "']", function(done) { 162 | var filter = new DimensionFilter().gt(num).make(); 163 | filter.expressions.should.eql([num.toString()]); 164 | done(); 165 | }); 166 | it("should equal to ['" + (num - 1) + "']", function(done) { 167 | var filter = new DimensionFilter().greaterThanEqualTo(num).make(); 168 | filter.expressions.should.eql([(num - 1).toString()]); 169 | done(); 170 | }); 171 | it("should equal to ['" + (num - 1) + "']", function(done) { 172 | var filter = new DimensionFilter().gte(num).make(); 173 | filter.expressions.should.eql([(num - 1).toString()]); 174 | done(); 175 | }); 176 | it("should equal to ['" + num + "']", function(done) { 177 | var filter = new DimensionFilter().lessThan(num).make(); 178 | filter.expressions.should.eql([num.toString()]); 179 | done(); 180 | }); 181 | it("should equal to ['" + num + "']", function(done) { 182 | var filter = new DimensionFilter().lt(num).make(); 183 | filter.expressions.should.eql([num.toString()]); 184 | done(); 185 | }); 186 | it("should equal to ['" + (num + 1) + "']", function(done) { 187 | var filter = new DimensionFilter().lessThanEqualTo(num).make(); 188 | filter.expressions.should.eql([(num + 1).toString()]); 189 | done(); 190 | }); 191 | it("should equal to ['" + (num + 1) + "']", function(done) { 192 | var filter = new DimensionFilter().lte(num).make(); 193 | filter.expressions.should.eql([(num + 1).toString()]); 194 | done(); 195 | }); 196 | it("should equal to ['1','2','3']", function(done) { 197 | var filter = new DimensionFilter().inList([1, 2, 3]).make(); 198 | filter.expressions.should.eql(["1", "2", "3"]); 199 | done(); 200 | }); 201 | it("should equal to ['1','2','3']", function(done) { 202 | var filter = new DimensionFilter().inList(1, 2, 3).make(); 203 | filter.expressions.should.eql(["1", "2", "3"]); 204 | done(); 205 | }); 206 | }); 207 | 208 | it("should have caseSensitive equal to true", function(done) { 209 | var filter = new DimensionFilter().caseSensitive().make(); 210 | filter.caseSensitive.should.equal(true); 211 | done(); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /src/ApiHelper/index.js: -------------------------------------------------------------------------------- 1 | var metrics = ["metric1","metric2","metric3","metric4","metric5","metric6","metric7","metric8","metric9","metric10","metric11","metric12","metric13","metric14","metric15","metric16","metric17","metric18","metric19","metric20","users","newUsers","percentNewSessions","1dayUsers","7dayUsers","14dayUsers","28dayUsers","30dayUsers","sessionsPerUser","visitors","newVisits","percentNewVisits","sessions","bounces","bounceRate","sessionDuration","avgSessionDuration","uniqueDimensionCombinations","hits","visits","visitBounceRate","organicSearches","impressions","adClicks","adCost","CPM","CPC","CTR","costPerTransaction","costPerGoalConversion","costPerConversion","RPC","ROAS","ROI","margin","goalXXStarts","goalStartsAll","goalXXCompletions","goalCompletionsAll","goalXXValue","goalValueAll","goalValuePerSession","goalXXConversionRate","goalConversionRateAll","goalXXAbandons","goalAbandonsAll","goalXXAbandonRate","goalAbandonRateAll","goalValuePerVisit","pageValue","entrances","entranceRate","pageviews","pageviewsPerSession","uniquePageviews","timeOnPage","avgTimeOnPage","exits","exitRate","pageviewsPerVisit","contentGroupUniqueViewsXX","searchResultViews","searchUniques","avgSearchResultViews","searchSessions","percentSessionsWithSearch","searchDepth","avgSearchDepth","searchRefinements","percentSearchRefinements","searchDuration","avgSearchDuration","searchExits","searchExitRate","searchGoalXXConversionRate","searchGoalConversionRateAll","goalValueAllPerSearch","searchVisits","percentVisitsWithSearch","pageLoadTime","pageLoadSample","avgPageLoadTime","domainLookupTime","avgDomainLookupTime","pageDownloadTime","avgPageDownloadTime","redirectionTime","avgRedirectionTime","serverConnectionTime","avgServerConnectionTime","serverResponseTime","avgServerResponseTime","speedMetricsSample","domInteractiveTime","avgDomInteractiveTime","domContentLoadedTime","avgDomContentLoadedTime","domLatencyMetricsSample","screenviews","uniqueScreenviews","screenviewsPerSession","timeOnScreen","avgScreenviewDuration","uniqueAppviews","totalEvents","uniqueEvents","eventValue","avgEventValue","sessionsWithEvent","eventsPerSessionWithEvent","visitsWithEvent","eventsPerVisitWithEvent","transactions","transactionsPerSession","transactionRevenue","revenuePerTransaction","transactionRevenuePerSession","transactionShipping","transactionTax","totalValue","itemQuantity","uniquePurchases","revenuePerItem","itemRevenue","itemsPerPurchase","localTransactionRevenue","localTransactionShipping","localTransactionTax","localItemRevenue","buyToDetailRate","cartToDetailRate","internalPromotionCTR","internalPromotionClicks","internalPromotionViews","localProductRefundAmount","localRefundAmount","productAddsToCart","productCheckouts","productDetailViews","productListCTR","productListClicks","productListViews","productRefundAmount","productRefunds","productRemovesFromCart","productRevenuePerPurchase","quantityAddedToCart","quantityCheckedOut","quantityRefunded","quantityRemovedFromCart","refundAmount","revenuePerUser","totalRefunds","transactionsPerUser","transactionsPerVisit","transactionRevenuePerVisit","socialInteractions","uniqueSocialInteractions","socialInteractionsPerSession","socialInteractionsPerVisit","userTimingValue","userTimingSample","avgUserTimingValue","exceptions","exceptionsPerScreenview","fatalExceptions","fatalExceptionsPerScreenview","metricXX","dcmFloodlightQuantity","dcmFloodlightRevenue","dcmCPC","dcmCTR","dcmClicks","dcmCost","dcmImpressions","dcmROAS","dcmRPC","dcmMargin","adsenseRevenue","adsenseAdUnitsViewed","adsenseAdsViewed","adsenseAdsClicks","adsensePageImpressions","adsenseCTR","adsenseECPM","adsenseExits","adsenseViewableImpressionPercent","adsenseCoverage","adxImpressions","adxCoverage","adxMonetizedPageviews","adxImpressionsPerSession","adxViewableImpressionsPercent","adxClicks","adxCTR","adxRevenue","adxRevenuePer1000Sessions","adxECPM","dfpImpressions","dfpCoverage","dfpMonetizedPageviews","dfpImpressionsPerSession","dfpViewableImpressionsPercent","dfpClicks","dfpCTR","dfpRevenue","dfpRevenuePer1000Sessions","dfpECPM","backfillImpressions","backfillCoverage","backfillMonetizedPageviews","backfillImpressionsPerSession","backfillViewableImpressionsPercent","backfillClicks","backfillCTR","backfillRevenue","backfillRevenuePer1000Sessions","backfillECPM","cohortActiveUsers","cohortAppviewsPerUser","cohortAppviewsPerUserWithLifetimeCriteria","cohortGoalCompletionsPerUser","cohortGoalCompletionsPerUserWithLifetimeCriteria","cohortPageviewsPerUser","cohortPageviewsPerUserWithLifetimeCriteria","cohortRetentionRate","cohortRevenuePerUser","cohortRevenuePerUserWithLifetimeCriteria","cohortSessionDurationPerUser","cohortSessionDurationPerUserWithLifetimeCriteria","cohortSessionsPerUser","cohortSessionsPerUserWithLifetimeCriteria","cohortTotalUsers","cohortTotalUsersWithLifetimeCriteria","correlationScore","queryProductQuantity","relatedProductQuantity","dbmCPA","dbmCPC","dbmCPM","dbmCTR","dbmClicks","dbmConversions","dbmCost","dbmImpressions","dbmROAS","dsCPC","dsCTR","dsClicks","dsCost","dsImpressions","dsProfit","dsReturnOnAdSpend","dsRevenuePerClick"]; 2 | var dimensions = ["dimension1","dimension2","dimension3","dimension4","dimension5","dimension6","dimension7","dimension8","dimension9","dimension10","dimension11","dimension12","dimension13","dimension14","dimension15","dimension16","dimension17","dimension18","dimension19","dimension20","userType","sessionCount","daysSinceLastSession","userDefinedValue","userBucket","visitorType","visitCount","sessionDurationBucket","visitLength","referralPath","fullReferrer","campaign","source","medium","sourceMedium","keyword","adContent","socialNetwork","hasSocialSourceReferral","campaignCode","adGroup","adSlot","adDistributionNetwork","adMatchType","adKeywordMatchType","adMatchedQuery","adPlacementDomain","adPlacementUrl","adFormat","adTargetingType","adTargetingOption","adDisplayUrl","adDestinationUrl","adwordsCustomerID","adwordsCampaignID","adwordsAdGroupID","adwordsCreativeID","adwordsCriteriaID","adQueryWordCount","isTrueViewVideoAd","goalCompletionLocation","goalPreviousStep1","goalPreviousStep2","goalPreviousStep3","browser","browserVersion","operatingSystem","operatingSystemVersion","mobileDeviceBranding","mobileDeviceModel","mobileInputSelector","mobileDeviceInfo","mobileDeviceMarketingName","deviceCategory","browserSize","dataSource","continent","subContinent","country","region","metro","city","latitude","longitude","networkDomain","networkLocation","cityId","continentId","countryIsoCode","metroId","regionId","regionIsoCode","subContinentCode","flashVersion","javaEnabled","language","screenColors","sourcePropertyDisplayName","sourcePropertyTrackingId","screenResolution","socialActivityContentUrl","hostname","pagePath","pagePathLevel1","pagePathLevel2","pagePathLevel3","pagePathLevel4","pageTitle","landingPagePath","secondPagePath","exitPagePath","previousPagePath","pageDepth","landingContentGroupXX","previousContentGroupXX","contentGroupXX","searchUsed","searchKeyword","searchKeywordRefinement","searchCategory","searchStartPage","searchDestinationPage","searchAfterDestinationPage","appInstallerId","appVersion","appName","appId","screenName","screenDepth","landingScreenName","exitScreenName","eventCategory","eventAction","eventLabel","transactionId","affiliation","sessionsToTransaction","daysToTransaction","productSku","productName","productCategory","currencyCode","checkoutOptions","internalPromotionCreative","internalPromotionId","internalPromotionName","internalPromotionPosition","orderCouponCode","productBrand","productCategoryHierarchy","productCategoryLevelXX","productCouponCode","productListName","productListPosition","productVariant","shoppingStage","visitsToTransaction","socialInteractionNetwork","socialInteractionAction","socialInteractionNetworkAction","socialInteractionTarget","socialEngagementType","userTimingCategory","userTimingLabel","userTimingVariable","exceptionDescription","experimentId","experimentVariant","dimensionXX","customVarNameXX","customVarValueXX","date","year","month","week","day","hour","minute","nthMonth","nthWeek","nthDay","nthMinute","dayOfWeek","dayOfWeekName","dateHour","dateHourMinute","yearMonth","yearWeek","isoWeek","isoYear","isoYearIsoWeek","nthHour","dcmClickAd","dcmClickAdId","dcmClickAdType","dcmClickAdTypeId","dcmClickAdvertiser","dcmClickAdvertiserId","dcmClickCampaign","dcmClickCampaignId","dcmClickCreativeId","dcmClickCreative","dcmClickRenderingId","dcmClickCreativeType","dcmClickCreativeTypeId","dcmClickCreativeVersion","dcmClickSite","dcmClickSiteId","dcmClickSitePlacement","dcmClickSitePlacementId","dcmClickSpotId","dcmFloodlightActivity","dcmFloodlightActivityAndGroup","dcmFloodlightActivityGroup","dcmFloodlightActivityGroupId","dcmFloodlightActivityId","dcmFloodlightAdvertiserId","dcmFloodlightSpotId","dcmLastEventAd","dcmLastEventAdId","dcmLastEventAdType","dcmLastEventAdTypeId","dcmLastEventAdvertiser","dcmLastEventAdvertiserId","dcmLastEventAttributionType","dcmLastEventCampaign","dcmLastEventCampaignId","dcmLastEventCreativeId","dcmLastEventCreative","dcmLastEventRenderingId","dcmLastEventCreativeType","dcmLastEventCreativeTypeId","dcmLastEventCreativeVersion","dcmLastEventSite","dcmLastEventSiteId","dcmLastEventSitePlacement","dcmLastEventSitePlacementId","dcmLastEventSpotId","userAgeBracket","userGender","interestOtherCategory","interestAffinityCategory","interestInMarketCategory","visitorAgeBracket","visitorGender","acquisitionCampaign","acquisitionMedium","acquisitionSource","acquisitionSourceMedium","acquisitionTrafficChannel","cohort","cohortNthDay","cohortNthMonth","cohortNthWeek","channelGrouping","correlationModelId","queryProductId","queryProductName","queryProductVariation","relatedProductId","relatedProductName","relatedProductVariation","dbmClickAdvertiser","dbmClickAdvertiserId","dbmClickCreativeId","dbmClickExchange","dbmClickExchangeId","dbmClickInsertionOrder","dbmClickInsertionOrderId","dbmClickLineItem","dbmClickLineItemId","dbmClickSite","dbmClickSiteId","dbmLastEventAdvertiser","dbmLastEventAdvertiserId","dbmLastEventCreativeId","dbmLastEventExchange","dbmLastEventExchangeId","dbmLastEventInsertionOrder","dbmLastEventInsertionOrderId","dbmLastEventLineItem","dbmLastEventLineItemId","dbmLastEventSite","dbmLastEventSiteId","dsAdGroup","dsAdGroupId","dsAdvertiser","dsAdvertiserId","dsAgency","dsAgencyId","dsCampaign","dsCampaignId","dsEngineAccount","dsEngineAccountId","dsKeyword","dsKeywordId"]; 3 | 4 | 5 | module.exports = (function() { 6 | var metricDimensionNames = {}; 7 | var metricDimensionData = {}; 8 | 9 | var helper = { 10 | generateApiName: function(name) { 11 | return `ga:${name}`; 12 | }, 13 | sortMetricsDimensions: function(values) { 14 | var that = this; 15 | 16 | var result = { 17 | metrics: [], 18 | dimensions: [] 19 | } 20 | 21 | values.forEach(function(value){ 22 | value = that.fixName(value); 23 | result[metricDimensionData[value]].push(value); 24 | }); 25 | 26 | return result; 27 | }, 28 | fixName: function(name) { 29 | name = name.toLowerCase(); 30 | if(!(name in metricDimensionNames)) { 31 | return name; 32 | } 33 | return metricDimensionNames[name]; 34 | }, 35 | register: function(id, name, type) { 36 | metricDimensionNames[id] = name; 37 | metricDimensionData[name] = type; 38 | }, 39 | registerMetric: function(name) { 40 | this.register(name.toLowerCase(),name,"metrics"); 41 | }, 42 | registerDimension: function(name) { 43 | this.register(name.toLowerCase(),name,"dimensions"); 44 | } 45 | }; 46 | 47 | for(metric of metrics) { 48 | helper.registerMetric(metric); 49 | } 50 | 51 | for(dimension of dimensions) { 52 | helper.registerDimension(dimension); 53 | } 54 | 55 | return helper; 56 | })() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Simple Google Analytics client for NodeJs 4 | 5 | It should be much easier to retrieve data from the Google Analytics API and this package helps you achieve that. Focus on analyzing the data let it handle the rest. 6 | 7 | This is still very much work in progress so please check back. 8 | 9 | **Note:** Recent v0.3.0 update removed the need to manually create filter objects. Please see the demo. 10 | 11 | ## Down to business 12 | 13 | 14 | Getting the top 10 links is as simple as this: 15 | 16 | ```JavaScript 17 | var analytics = new SimpleGA("./key.json"); 18 | 19 | var request = Request() 20 | .select("pagepath","pageviews") 21 | .from(12345678) 22 | .orderDesc("pageviews") 23 | .results(10); 24 | 25 | var data = await analytics.run(request); 26 | ``` 27 | 28 | By default, data will be returned as an array of objects in the format below. For the top 10 list the result would look similar to this: 29 | 30 | ```JavaScript 31 | [ 32 | { 33 | pagePath: "/", 34 | pageviews: 123 35 | }, 36 | ... 37 | ] 38 | ``` 39 | 40 | ## What it really is 41 | **node-simple-ga** helps you create and make [Reporting API v4 compliant](https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet) JSON requests in a function-oriented manner, parse the response, and paginate additional requests if requested by the user. Further improvements will be focused on creating requests in a more robust and efficient way. 42 | ## What it won't be 43 | This package is not and will not be a data processing package. Data processing is left up to you - the developer. 44 | ## Installation 45 | To use the package, run: 46 | ```JavaScript 47 | npm i node-simple-ga 48 | ``` 49 | Before using the package, you must create and set up a [Service Account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount). You can also watch a video tutorial on [how to set up a Service account](https://www.youtube.com/watch?v=r6cWB0xnOwE). While the title says it's a PHP tutorial, it doesn't really matter because you won't be using PHP anyway. Focus on the account creation and granting read access to the service account. 50 | ## Usage 51 | Typical usage for the script follows the following script: 52 | 1) Require node-simple-ga 53 | 2) Initialize the analytics package by providing a valid service account key (see Installation) 54 | 3) Create a request object 55 | 4) Add metrics and dimensions 56 | 6) Add filters 57 | 7) Specify the sort order 58 | 8) Run the request 59 | 60 | Optionally: 61 | 1) Clone the request object 62 | 2) Make changes to the request object 63 | 3) Run the request 64 | 65 | ## Example 66 | * Return all pages of two views XXXXXXXX and YYYYYYYY who don't have at least 100 pageviews, 67 | * Show page urls, pageviews and users, 68 | * Make sure page url doesn't contain /archive/, 69 | * Load only those entries that came from the US, 70 | * Sort results by the amount of users in a descending order 71 | * Get only the top 100 results from XXXXXXXX 72 | * For view YYYYYYYY also get the amount of sessions 73 | 74 | ```JavaScript 75 | const { 76 | SimpleGA, 77 | Request 78 | } = require("node-simple-ga"); 79 | 80 | (async function() { 81 | var analytics = new SimpleGA("./key.json"); 82 | 83 | var request1 = Request() 84 | .select("pagepath","pageviews","users") 85 | .from(XXXXXXXX) 86 | .where("pagepath").not().contains("/archive/") 87 | .where("country").is("US") 88 | .where("pageviews").lessThan(101) 89 | .results(100) 90 | .orderDesc("users"); 91 | 92 | var request2 = request1.clone() 93 | .select("sessions") 94 | .from(YYYYYYYY) 95 | .everything(); 96 | 97 | try { 98 | var [data1, data2] = await Promise.all([ 99 | analytics.run(request1), 100 | analytics.run(request2) 101 | ]); 102 | } catch (err) { 103 | console.error(err); 104 | } 105 | })(); 106 | ``` 107 | Since it's not possible to use negative lookups in Google Analytics (e.g. urls that don't contain something), you must first look up all urls with the thing you want to avoid and then negate the operation (in this case with .not()) 108 | 109 | Please note that if you don't specify a date, only the last 7 days, excluding today, will be processed. 110 | 111 | > If a date range is not provided, the default date range is (startDate: current date - 7 days, endDate: current date - 1 day). 112 | > [Source](https://developers.google.com/analytics/devguides/reporting/core/v4/rest/v4/reports/batchGet#ReportRequest.FIELDS) 113 | 114 | ## Reference 115 | When specifying a dimension or a metric, you don't have to add ga: at the beginning and you may enter it in a case insensitive manner; the system will fix it for you i.e. pagepath becomes ga:pagePath when querying Google Analytics. 116 | 117 | Before processing data, you should know the difference between dimensions and metrics. [Read more about it here.](https://support.google.com/analytics/answer/1033861?hl=en) 118 | 119 | ### Request functions 120 | 121 | #### select(*keys*), select([*keys*]), fetch(*keys*), fetch([*keys*]) 122 | 123 | Specify in a case-insensitive manner which dimensions and metrics you're going to need. You can pass both, an array or a list of metrics or dimensions. It's useful if you generate metrics dynamically. However, if you pass a custom key, such as a computed metric, it's up to you to ensure it's written correctly. 124 | 125 | ```Javascript 126 | select("pagepath","sessions","users") 127 | ``` 128 | is the same as 129 | ```Javascript 130 | select(["pageviews","sessions","users"]) 131 | ``` 132 |