|
| 1 | +/** |
| 2 | + * Copyright 2018 Google Inc. All rights reserved. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +'use strict'; |
| 18 | + |
| 19 | +import fs from 'fs'; |
| 20 | +import url from 'url'; |
| 21 | +const URL = url.URL; |
| 22 | +import GoogleAPIs from 'googleapis'; |
| 23 | +const google = GoogleAPIs.google; |
| 24 | +// import GoogleAuth from 'google-auth-library'; |
| 25 | + |
| 26 | +let CACHE = new Map(); |
| 27 | + |
| 28 | +const START_DATE = '2011-01-01'; // Beginning date to fetch all analytics data for. |
| 29 | +const MIN_PAGEVIEWS = 10; |
| 30 | + |
| 31 | +const VIEW_IDS = { |
| 32 | + robdodson: { |
| 33 | + viewId: '57149356', |
| 34 | + notPathRegexs: ['^/tag/', '^/blog/categories/', '^/author/rob/', '^/blog/page/', '^/page/'], |
| 35 | + }, |
| 36 | + ericbidelman: { |
| 37 | + viewId: '48771992', |
| 38 | + pathRegexs: ['^/post/', '/post/14636214755'], |
| 39 | + removeFromTitles: ' - Eric Bidelman', |
| 40 | + }, |
| 41 | + // webfu: {viewId: '88450368'}, |
| 42 | +}; |
| 43 | + |
| 44 | +// const creds = JSON.parse(fs.readFileSync('./google_oauth_credentials.json')); |
| 45 | +// const API_KEY = 'AIzaSyCzhvPbNswHA1TXcqtSF0aiVj7O3oi9BfM'; |
| 46 | +// const app = await google.auth.getApplicationDefault(); |
| 47 | + |
| 48 | +class Analytics { |
| 49 | + static get SCOPES() { |
| 50 | + return ['https://www.googleapis.com/auth/analytics.readonly']; |
| 51 | + } |
| 52 | + |
| 53 | + constructor(authClient) { |
| 54 | + this.api = google.analyticsreporting({ |
| 55 | + version: 'v4', |
| 56 | + auth: authClient, // API_KEY, |
| 57 | + }); |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Creates a new map with titles mapped to results. |
| 62 | + * @param {!Map} map |
| 63 | + */ |
| 64 | + static toTitleMap(map) { |
| 65 | + const titleMap = new Map(); |
| 66 | + for (const [path, result] of map) { |
| 67 | + titleMap.set(result.title, result); |
| 68 | + } |
| 69 | + return titleMap; |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * Fetch results from the Reporting API. |
| 74 | + * @param {!Object=} query |
| 75 | + * @return {!{report: !Object, headers: !Array<{type: string, name: string}>}} |
| 76 | + */ |
| 77 | + async query({viewId, startDate = '30daysAgo', endDate = 'yesterday', |
| 78 | + pathRegexs = ['/'], notPathRegexs = []} = {}) { |
| 79 | + const query = { |
| 80 | + viewId, |
| 81 | + dateRanges: [{startDate, endDate}], |
| 82 | + metrics: [{expression: 'ga:pageviews'}, {expression: 'ga:users'}], |
| 83 | + metricFilterClauses: [{ |
| 84 | + filters: [{ |
| 85 | + metricName: 'ga:pageviews', |
| 86 | + operator: 'GREATER_THAN', |
| 87 | + comparisonValue: String(MIN_PAGEVIEWS), |
| 88 | + }] |
| 89 | + }], |
| 90 | + dimensions: [{name: 'ga:pageTitle'}, {name: 'ga:pagePath'}], |
| 91 | + orderBys: [{fieldName: 'ga:pageviews', sortOrder: 'DESCENDING'}], |
| 92 | + dimensionFilterClauses: [{ |
| 93 | + operator: 'AND', |
| 94 | + filters: [], |
| 95 | + }], |
| 96 | + }; |
| 97 | + |
| 98 | + // Filter by user-provided path regexs. |
| 99 | + pathRegexs.forEach(regex => query.dimensionFilterClauses[0].filters.push({ |
| 100 | + dimensionName: 'ga:pagePath', |
| 101 | + operator: 'REGEXP', |
| 102 | + expressions: pathRegexs, |
| 103 | + })); |
| 104 | + |
| 105 | + // Filter out some additional paths. |
| 106 | + notPathRegexs.forEach(regex => query.dimensionFilterClauses[0].filters.push({ |
| 107 | + dimensionName: 'ga:pagePath', |
| 108 | + not: true, |
| 109 | + operator: 'REGEXP', |
| 110 | + expressions: [regex], |
| 111 | + })); |
| 112 | + |
| 113 | + const resp = await this.api.reports.batchGet({ |
| 114 | + resource: {reportRequests: [query]} |
| 115 | + }); |
| 116 | + |
| 117 | + const report = resp.data.reports[0]; |
| 118 | + const headers = report.columnHeader.metricHeader.metricHeaderEntries; |
| 119 | + const urlMap = new Map(); |
| 120 | + |
| 121 | + if (!report.data.rowCount) { |
| 122 | + return {results: urlMap, headers, startDate, endDate, rowCount: 0}; |
| 123 | + } |
| 124 | + |
| 125 | + report.data.rows.forEach((row, i) => { |
| 126 | + row.metrics.forEach((metric, j) => { |
| 127 | + const [title, path] = row.dimensions; |
| 128 | + const [pageviews, users] = metric.values.map(val => parseInt(val)); |
| 129 | + |
| 130 | + // If a URL has already been seen, add to its pageviews. Ignore query params. |
| 131 | + const pathWithoutParams = new URL(path, 'http://dummydomain.com').pathname; |
| 132 | + const item = urlMap.get(pathWithoutParams); |
| 133 | + if (item) { |
| 134 | + item.pageviews += pageviews; |
| 135 | + item.users += users; |
| 136 | + } else { |
| 137 | + urlMap.set(pathWithoutParams, {title, path: pathWithoutParams, pageviews, users}); |
| 138 | + } |
| 139 | + }); |
| 140 | + }); |
| 141 | + |
| 142 | + return { |
| 143 | + results: urlMap, |
| 144 | + headers, |
| 145 | + startDate, |
| 146 | + endDate, |
| 147 | + rowCount: report.data.rowCount |
| 148 | + }; |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +let authClient = google.auth.fromJSON(JSON.parse( |
| 153 | + fs.readFileSync('./analyticsServiceAccountKey.json'))); |
| 154 | +// authClient.scopes = Analytics.SCOPES; |
| 155 | +if (authClient.createScopedRequired && authClient.createScopedRequired()) { |
| 156 | + authClient = authClient.createScoped(Analytics.SCOPES); |
| 157 | +} |
| 158 | + |
| 159 | +/** |
| 160 | + * @param {boolean=} clearCache Whether to clear the cache. True by default. |
| 161 | + * @return {Promise<!Map>} |
| 162 | + */ |
| 163 | +async function updateAnalyticsData(clearCache = false) { |
| 164 | + if (CACHE.size && !clearCache) { |
| 165 | + return CACHE; |
| 166 | + } |
| 167 | + |
| 168 | + console.info('Updating Analytics data...'); |
| 169 | + const tic = Date.now(); |
| 170 | + |
| 171 | + await authClient.authorize(); |
| 172 | + |
| 173 | + const ga = new Analytics(authClient); |
| 174 | + |
| 175 | + const merged = new Map(); |
| 176 | + |
| 177 | + for (const [user, config] of Object.entries(VIEW_IDS)) { |
| 178 | + const result = await ga.query({ |
| 179 | + viewId: config.viewId, |
| 180 | + startDate: START_DATE, |
| 181 | + endDate: (new Date()).toJSON().split('T')[0], |
| 182 | + pathRegexs: config.pathRegexs, |
| 183 | + notPathRegexs: config.notPathRegexs, |
| 184 | + }); |
| 185 | + result.results.forEach(item => { |
| 186 | + if ('removeFromTitles' in config) { |
| 187 | + item.title = item.title.replace(config.removeFromTitles, ''); |
| 188 | + } |
| 189 | + merged.set(item.path, item); |
| 190 | + }); |
| 191 | + } |
| 192 | + |
| 193 | + // Sort by pageviews. |
| 194 | + const results = new Map([...merged.entries()] |
| 195 | + .sort((a, b) => b[1].pageviews - a[1].pageviews)); |
| 196 | + |
| 197 | + CACHE = results; |
| 198 | + |
| 199 | + console.info(`Analytics update took ${(Date.now() - tic)/1000}s`); |
| 200 | + |
| 201 | + return results; |
| 202 | +} |
| 203 | + |
| 204 | +export {Analytics, updateAnalyticsData}; |
| 205 | + |
| 206 | +// (async() => { |
| 207 | + |
| 208 | +// // const oauth2Client = new google.auth.OAuth2();//creds.client_id, creds.client_secret, ''); |
| 209 | +// // oauth2Client.setCredentials(creds); |
| 210 | +// // oauth2Client.apiKey = API_KEY; |
| 211 | +// // google.options({auth: oauth2Client}); |
| 212 | + |
| 213 | +// // const creds = await GoogleAuth.auth.getCredentials(); |
| 214 | +// const allResults = await updateAnalyticsData(); |
| 215 | +// let i = 1; |
| 216 | +// for (const [path, result] of allResults) { |
| 217 | +// const {title, path, pageviews, users} = result; |
| 218 | +// console.log(`${i++}. ${path} ${formatNumber(pageviews)} views, ${formatNumber(users)} users`); |
| 219 | +// } |
| 220 | + |
| 221 | +// })(); |
0 commit comments