UNPKG

trendie

Version:

CLI tool that displays Internet Explorer usage and trends for your site via Google Analytics.

502 lines (356 loc) 12 kB
// modules var GA = require('googleanalytics'); var Q = require('q'); var moment = require('moment'); var _ = require('lodash'); var numeral = require('numeral'); var Table = require('cli-table'); var Progger = require('progger'); var colors = require('colors'); var prettyjson = require('prettyjson'); // checks version against IE whitelist var isValidIe = function (version) { var whitelist = [ '11.0', '10.0', '9.0', '8.0', '7.0', '6.0' ]; return whitelist.indexOf(version) >= 0; }; // configuration for prettyjson output colors var getPrettyjsonConfig = function () { return { keysColor: 'green', dashColor: 'yellow', stringColor: 'white' }; }; // returns a GA request options object var getGaRequestOpts = function (profileId, startDate, endDate, metrics, dimensions) { var opts = { 'ids': 'ga:' + profileId, 'start-date': startDate, 'end-date': endDate, 'metrics': metrics, 'dimensions': dimensions }; return opts; }; // removes 'Visits' or 'Browsers' from the end of a word var truncateDescription = function (description) { var re = /(Visits|Browsers)/g, newDescription; newDescription = description.replace(re, ''); return newDescription; }; // returns an directional arrow var getArrow = function (direction) { var up = '⬆'.green; var down = '⬇'.red; if (direction === 'up') { return up; } return down; }; // returns a percent string var percent = function (dividend, divisor) { var p = dividend / divisor; return numeral(p).format('0.000%').white; }; var isNotNumber = function (num) { return num !== num; }; var isInfinity = function (num) { return (num*num === num) && (0/num === 0); }; // returns a directional arrow & color-formated percent change var percentChange = function (initial, final) { var change; var noChange = false; var positive = true; var init = parseInt(initial, 10); var fin = parseInt(final, 10); change = ((fin - init) / Math.abs(init)); // need these checks because two consecutive periods with no visits by a browser // results in division by zero above or a period with no visits followed by one with // some visits results in Infinity if (isNotNumber(change)) { change = 0; } if (isInfinity(change)) { change = 1; } if (change < 0) { positive = false; } if (change < 0.0005 && change > -0.0005) { noChange = true; } // format into percentage change = numeral(change).format('0,0.0%'); // format with or without directional arrows if (noChange) { return change.white; } if (positive) { return getArrow('up') + ' ' + change.green; } return getArrow('down') + ' ' + change.red; }; // returns array of IE versions from data var getIeVersions = function (data) { var versions = []; _.each(data, function (key, value) { _.each(data[value].ieVisits, function (key, value) { versions.push(key.version); }); }); // keep only unique values versions = _.uniq(versions); // sort version strings descending versions = versions.sort(function (a, b) { return b - a; }); return versions; }; // returns a formatted row for the cli table var createRow = function (title, visitsLastMonth, visitsSixMonthsAgo, visitsLastYear, showPercentOfTotal, total) { var row = {}; var columns = []; var percentOfTotal = ''; if (showPercentOfTotal) { percentOfTotal = ' (' + percent(visitsLastMonth, total) + ')'; } // past month if (typeof visitsLastMonth !== 'undefined') { columns.push(numeral(visitsLastMonth).format('0,0') + percentOfTotal); } // 6 months ago if (typeof visitsSixMonthsAgo !== 'undefined') { columns.push(numeral(visitsSixMonthsAgo).format('0,0') + ' ' + percentChange(visitsSixMonthsAgo, visitsLastMonth)); } // a year ago if (typeof visitsLastYear !== 'undefined') { columns.push(numeral(visitsLastYear).format('0,0') + ' ' + percentChange(visitsLastYear, visitsLastMonth)); } row[title] = columns; return row; }; // returns the count of browser occurrence var getVersionCount = function (visits, ver) { var count = _.find(visits, { 'version': ver }); if (typeof count === 'undefined') { count = { count: 0 }; } return count.count; }; // returns an array of rows that can be inserted into the cli table var getTableRows = function (data) { var visits = {}; var ieVisits = {}; var rows = []; var versions = getIeVersions(data); _.forEach(data, function (val, prop) { visits[prop] = val.totalVisits; }); // add row for all browsers rows.push(createRow('All Browsers', visits.lastMonth, visits.sixMonthsAgo, visits.yearAgo, false, null)); // add rows for IE browsers _.each(versions, function (ver) { _.each(data, function (val, prop) { ieVisits[prop] = getVersionCount(val.ieVisits, ver); }); rows.push(createRow('IE ' + ver.cyan, ieVisits.lastMonth, ieVisits.sixMonthsAgo, ieVisits.yearAgo, true, visits.lastMonth)); }); return rows; }; // returns the data needed to create a cli table var getCliTable = function (data) { var table; var header = ['']; var headerMap = { lastMonth: 'Past Month', sixMonthsAgo: '6 Months Ago', yearAgo: 'Year Ago' }; // dynamically build the table header (for when there's not enough past data) _.each(_.keys(data), function (val) { header.push(headerMap[val]); }); // create a new table header table = new Table({ style: { 'padding-left': 1, 'padding-right': 1, 'head': ['blue'] }, head: header }); _.each(getTableRows(data), function (row) { table.push(row); }); return table; }; // returns an object from the large data chunk that's returned from GA var processResults = function (results) { var processed = {}, description, browser, version, versionCount; _.each(results, function (result, i) { // if there's no data bail out of this loop if (result.entries.length === 0) { return; // return console.error('\nno results for ' + truncateDescription(result.description)); } // gets a property name for the processed object description = truncateDescription(result.description); // adds property if it doesn't already exist if (!processed.hasOwnProperty(description)) { processed[description] = {}; } // if there's no dimensions, then we've got visits not browsers if (typeof result.entries[0].dimensions[0]['ga:browser'] === 'undefined') { processed[description].totalVisits = result.entries[0].metrics[0]['ga:visits']; } else { processed[description].ieVisits = []; _.each(result.entries, function (entry, i) { browser = entry.dimensions[0]['ga:browser']; version = entry.dimensions[0]['ga:browserVersion']; if (browser === 'Internet Explorer' && isValidIe(version)) { versionCount = { name: browser, version: version, count: entry.metrics[0]['ga:visits'] }; processed[description].ieVisits.push(versionCount); } }); } }); return processed; }; // main function: gets GA data and outputs to console exports.getTrends = function (profileId, username, password, jsonOut) { // local vars var dateFormat = 'YYYY-MM-DD'; var dateToday = moment().format(dateFormat); var dateOneMonthAgo = moment(dateToday).subtract(28, 'days').format(dateFormat); var dateSixMonthsAgo = moment(dateToday).subtract(183, 'days').format(dateFormat); var dateSevenMonthsAgo = moment(dateSixMonthsAgo).subtract(28, 'days').format(dateFormat); var dateYearAgo = moment(dateToday).subtract(365, 'days').format(dateFormat); var dateYearAndMonthAgo = moment(dateYearAgo).subtract(28, 'days').format(dateFormat); var gaRequestOpts = { lastMonthVisits: getGaRequestOpts(profileId, dateOneMonthAgo, dateToday, 'ga:visits', ''), lastMonthBrowsers: getGaRequestOpts(profileId, dateOneMonthAgo, dateToday, 'ga:visits', 'ga:browser,ga:browserVersion'), sixMonthsAgoVisits: getGaRequestOpts(profileId, dateSevenMonthsAgo, dateSixMonthsAgo, 'ga:visits', ''), sixMonthsAgoBrowsers: getGaRequestOpts(profileId, dateSevenMonthsAgo, dateSixMonthsAgo, 'ga:visits', 'ga:browser,ga:browserVersion'), yearAgoVisits: getGaRequestOpts(profileId, dateYearAndMonthAgo, dateYearAgo, 'ga:visits', ''), yearAgoBrowsers: getGaRequestOpts(profileId, dateYearAndMonthAgo, dateYearAgo, 'ga:visits', 'ga:browser,ga:browserVersion') }; var errMsgSent = false; var progress = new Progger(); var ga; // @todo: this sucks // I think the only uncaught is with GA logins failing (ie. credentials are incorrect) process.on('uncaughtException', function (err) { // stop progress ticker if it's running if (typeof progress === 'object' && progress.isRunning()) { progress.stop(); } if (err.message) { return console.error('\n' + err.message.red); } return console.error('\nAn unknown error has occurred!'.red); }); // returns a promise for a GA request function gaRequest(description) { var d = Q.defer(); ga.get(gaRequestOpts[description], function (err, entries) { // reject promise or resolve with entries if (err) { // this is a hacky way of preventing error message displaying multiple times if (err.message === 'User does not have sufficient permissions for this profile.') { if (!errMsgSent) { progress.stop(); errMsgSent = true; console.log('\n' + 'This GA user does not have permissions to read profile: '.red + profileId); } d.reject(''); } else { d.reject(err); } } else { d.resolve({description: description, entries: entries}); } }); return d.promise; } // start timer console.time('time-to-run'); if (!jsonOut) { process.stdout.write('\nDownloading data from Google Analytics'); } // start progress ticker progress.start(); // create new google analytics object ga = new GA.GA({'user': username, 'password': password}); // start GA session ga.login(function (err, token) { if (err) { return console.log('error logging in to Google Analytics'.error); } // complete a queue of promise-returning functions Q.allSettled([ gaRequest('lastMonthVisits'), gaRequest('lastMonthBrowsers'), gaRequest('sixMonthsAgoVisits'), gaRequest('sixMonthsAgoBrowsers'), gaRequest('yearAgoVisits'), gaRequest('yearAgoBrowsers') ]) .then(function (results) { var passed = []; results.forEach(function (result) { if (result.state === "fulfilled") { passed.push(result.value); } else { console.error(result.reason); } }); return processResults(passed); }) .then(function (processed) { if (jsonOut) { process.stdout.write('\n\n' + JSON.stringify(processed, null, ' ')); } // get table for printing to the command line return getCliTable(processed); }) .then(function (table) { // stop progress ticker progress.stop(); console.log('\n'); // if there were errors getting the data don't output empty table if (table.length <= 1) { return console.error('No data available!'.red); } if (!jsonOut) { console.log(table.toString()); } }) .fail(function (error) { // stop progress ticker progress.stop(); // if any errors happened in the promise-chain, output here console.log('\n'); console.error('fail: ', error.stack); }) .finally(function () { // end timer if (!jsonOut) { console.log('\n'); console.timeEnd('time-to-run'); } }); }); };