Prepare to modify / refactor analytics (#970)

- Move analytics into their own file
- Add test of analytics endpoint
This commit is contained in:
Paul Melnikow
2017-04-28 01:06:06 -04:00
committed by GitHub
parent c3ef232bf7
commit f47aa968cd
3 changed files with 155 additions and 114 deletions

115
lib/analytics.js Normal file
View File

@@ -0,0 +1,115 @@
const fs = require('fs');
// We can either use a process-wide object regularly saved to a JSON file,
// or a Redis equivalent (for multi-process / when the filesystem is unreliable.
var redis;
var useRedis = false;
if (process.env.REDISTOGO_URL) {
var redisToGo = require('url').parse(process.env.REDISTOGO_URL);
redis = require('redis').createClient(redisToGo.port, redisToGo.hostname);
redis.auth(redisToGo.auth.split(':')[1]);
useRedis = true;
}
var analytics = {};
var analyticsAutoSaveFileName = process.env.SHIELDS_ANALYTICS_FILE || './analytics.json';
var analyticsAutoSavePeriod = 10000;
setInterval(function analyticsAutoSave() {
if (useRedis) {
redis.set(analyticsAutoSaveFileName, JSON.stringify(analytics));
} else {
fs.writeFileSync(analyticsAutoSaveFileName, JSON.stringify(analytics));
}
}, analyticsAutoSavePeriod);
function defaultAnalytics() {
var analytics = Object.create(null);
// In case something happens on the 36th.
analytics.vendorMonthly = new Array(36);
resetMonthlyAnalytics(analytics.vendorMonthly);
analytics.rawMonthly = new Array(36);
resetMonthlyAnalytics(analytics.rawMonthly);
analytics.vendorFlatMonthly = new Array(36);
resetMonthlyAnalytics(analytics.vendorFlatMonthly);
analytics.rawFlatMonthly = new Array(36);
resetMonthlyAnalytics(analytics.rawFlatMonthly);
analytics.vendorFlatSquareMonthly = new Array(36);
resetMonthlyAnalytics(analytics.vendorFlatSquareMonthly);
analytics.rawFlatSquareMonthly = new Array(36);
resetMonthlyAnalytics(analytics.rawFlatSquareMonthly);
return analytics;
}
// Auto-load analytics.
function analyticsAutoLoad() {
var defaultAnalyticsObject = defaultAnalytics();
if (useRedis) {
redis.get(analyticsAutoSaveFileName, function(err, value) {
if (err == null && value != null) {
// if/try/return trick:
// if error, then the rest of the function is run.
try {
analytics = JSON.parse(value);
// Extend analytics with a new value.
for (var key in defaultAnalyticsObject) {
if (!(key in analytics)) {
analytics[key] = defaultAnalyticsObject[key];
}
}
return;
} catch(e) {
console.error('Invalid Redis analytics, resetting.');
console.error(e);
}
}
analytics = defaultAnalyticsObject;
});
} else {
// Not using Redis.
try {
analytics = JSON.parse(fs.readFileSync(analyticsAutoSaveFileName));
// Extend analytics with a new value.
for (var key in defaultAnalyticsObject) {
if (!(key in analytics)) {
analytics[key] = defaultAnalyticsObject[key];
}
}
} catch(e) {
if (e.code !== 'ENOENT') {
console.error('Invalid JSON file for analytics, resetting.');
console.error(e);
}
analytics = defaultAnalyticsObject;
}
}
}
var lastDay = (new Date()).getDate();
function resetMonthlyAnalytics(monthlyAnalytics) {
for (var i = 0; i < monthlyAnalytics.length; i++) {
monthlyAnalytics[i] = 0;
}
}
function incrMonthlyAnalytics(monthlyAnalytics) {
try {
var currentDay = (new Date()).getDate();
// If we changed month, reset empty days.
while (lastDay !== currentDay) {
// Assumption: at least a hit a month.
lastDay = (lastDay + 1) % monthlyAnalytics.length;
monthlyAnalytics[lastDay] = 0;
}
monthlyAnalytics[currentDay]++;
} catch(e) { console.error(e.stack); }
}
function getAnalytics() {
return analytics;
}
module.exports = {
analyticsAutoLoad,
incrMonthlyAnalytics,
getAnalytics
};

127
server.js
View File

@@ -25,7 +25,6 @@ var tryUrl = require('url').format({
console.log(tryUrl);
var domain = require('domain');
var request = require('request');
var fs = require('fs');
var LruCache = require('./lib/lru-cache.js');
var badge = require('./lib/badge.js');
var svg2img = require('./lib/svg-to-img.js');
@@ -56,6 +55,11 @@ const {
floorCount: floorCountColor,
version: versionColor,
} = require('./lib/color-formatters.js');
const {
analyticsAutoLoad,
incrMonthlyAnalytics,
getAnalytics
} = require('./lib/analytics');
var semver = require('semver');
var serverStartTime = new Date((new Date()).toGMTString());
@@ -64,115 +68,10 @@ var validTemplates = ['default', 'plastic', 'flat', 'flat-square', 'social'];
var darkBackgroundTemplates = ['default', 'flat', 'flat-square'];
var logos = loadLogos();
// Analytics
// We can either use a process-wide object regularly saved to a JSON file,
// or a Redis equivalent (for multi-process / when the filesystem is unreliable.
var redis;
var useRedis = false;
if (process.env.REDISTOGO_URL) {
var redisToGo = require('url').parse(process.env.REDISTOGO_URL);
redis = require('redis').createClient(redisToGo.port, redisToGo.hostname);
redis.auth(redisToGo.auth.split(':')[1]);
useRedis = true;
}
var analytics = {};
var analyticsAutoSaveFileName = process.env.SHIELDS_ANALYTICS_FILE || './analytics.json';
var analyticsAutoSavePeriod = 10000;
setInterval(function analyticsAutoSave() {
if (useRedis) {
redis.set(analyticsAutoSaveFileName, JSON.stringify(analytics));
} else {
fs.writeFileSync(analyticsAutoSaveFileName, JSON.stringify(analytics));
}
}, analyticsAutoSavePeriod);
function defaultAnalytics() {
var analytics = Object.create(null);
// In case something happens on the 36th.
analytics.vendorMonthly = new Array(36);
resetMonthlyAnalytics(analytics.vendorMonthly);
analytics.rawMonthly = new Array(36);
resetMonthlyAnalytics(analytics.rawMonthly);
analytics.vendorFlatMonthly = new Array(36);
resetMonthlyAnalytics(analytics.vendorFlatMonthly);
analytics.rawFlatMonthly = new Array(36);
resetMonthlyAnalytics(analytics.rawFlatMonthly);
analytics.vendorFlatSquareMonthly = new Array(36);
resetMonthlyAnalytics(analytics.vendorFlatSquareMonthly);
analytics.rawFlatSquareMonthly = new Array(36);
resetMonthlyAnalytics(analytics.rawFlatSquareMonthly);
return analytics;
}
// Auto-load analytics.
function analyticsAutoLoad() {
var defaultAnalyticsObject = defaultAnalytics();
if (useRedis) {
redis.get(analyticsAutoSaveFileName, function(err, value) {
if (err == null && value != null) {
// if/try/return trick:
// if error, then the rest of the function is run.
try {
analytics = JSON.parse(value);
// Extend analytics with a new value.
for (var key in defaultAnalyticsObject) {
if (!(key in analytics)) {
analytics[key] = defaultAnalyticsObject[key];
}
}
return;
} catch(e) {
console.error('Invalid Redis analytics, resetting.');
console.error(e);
}
}
analytics = defaultAnalyticsObject;
});
} else {
// Not using Redis.
try {
analytics = JSON.parse(fs.readFileSync(analyticsAutoSaveFileName));
// Extend analytics with a new value.
for (var key in defaultAnalyticsObject) {
if (!(key in analytics)) {
analytics[key] = defaultAnalyticsObject[key];
}
}
} catch(e) {
if (e.code !== 'ENOENT') {
console.error('Invalid JSON file for analytics, resetting.');
console.error(e);
}
analytics = defaultAnalyticsObject;
}
}
}
var lastDay = (new Date()).getDate();
function resetMonthlyAnalytics(monthlyAnalytics) {
for (var i = 0; i < monthlyAnalytics.length; i++) {
monthlyAnalytics[i] = 0;
}
}
function incrMonthlyAnalytics(monthlyAnalytics) {
try {
var currentDay = (new Date()).getDate();
// If we changed month, reset empty days.
while (lastDay !== currentDay) {
// Assumption: at least a hit a month.
lastDay = (lastDay + 1) % monthlyAnalytics.length;
monthlyAnalytics[lastDay] = 0;
}
monthlyAnalytics[currentDay]++;
} catch(e) { console.error(e.stack); }
}
analyticsAutoLoad();
camp.ajax.on('analytics/v1', function(json, end) { end(getAnalytics()); });
var suggest = require('./lib/suggest.js');
camp.ajax.on('analytics/v1', function(json, end) { end(analytics); });
camp.ajax.on('suggest/v1', suggest);
// Cache
@@ -212,11 +111,11 @@ function cache(f) {
var date = (reqTime).toGMTString();
ask.res.setHeader('Expires', date); // Proxies, GitHub, see #221.
ask.res.setHeader('Date', date);
incrMonthlyAnalytics(analytics.vendorMonthly);
incrMonthlyAnalytics(getAnalytics().vendorMonthly);
if (data.style === 'flat') {
incrMonthlyAnalytics(analytics.vendorFlatMonthly);
incrMonthlyAnalytics(getAnalytics().vendorFlatMonthly);
} else if (data.style === 'flat-square') {
incrMonthlyAnalytics(analytics.vendorFlatSquareMonthly);
incrMonthlyAnalytics(getAnalytics().vendorFlatSquareMonthly);
}
var cacheIndex = match[0] + '?label=' + data.label + '&style=' + data.style
@@ -6236,11 +6135,11 @@ function(data, match, end, ask) {
var color = escapeFormat(match[6]);
var format = match[8];
incrMonthlyAnalytics(analytics.rawMonthly);
incrMonthlyAnalytics(getAnalytics().rawMonthly);
if (data.style === 'flat') {
incrMonthlyAnalytics(analytics.rawFlatMonthly);
incrMonthlyAnalytics(getAnalytics().rawFlatMonthly);
} else if (data.style === 'flat-square') {
incrMonthlyAnalytics(analytics.rawFlatSquareMonthly);
incrMonthlyAnalytics(getAnalytics().rawFlatSquareMonthly);
}
// Cache management - the badge is constant.

View File

@@ -5,6 +5,7 @@ var fs = require('fs');
var path = require('path');
var isPng = require('is-png');
var isSvg = require('is-svg');
const fetch = require('node-fetch');
var svg2img = require('./lib/svg-to-img');
const serverHelpers = require('./lib/in-process-server-test-helpers');
@@ -68,4 +69,30 @@ describe('The server', function () {
});
});
});
describe('analytics endpoint', function () {
it('should return analytics in the expected format', function () {
return fetch(`${url}$analytics/v1`)
.then(res => {
assert(res.ok);
return res.json();
}).then(json => {
const keys = Object.keys(json);
const expectedKeys = [
'vendorMonthly',
'rawMonthly',
'vendorFlatMonthly',
'rawFlatMonthly',
'vendorFlatSquareMonthly',
'rawFlatSquareMonthly',
];
assert.deepEqual(keys.sort(), expectedKeys.sort());
keys.forEach(k => {
assert.ok(Array.isArray(json[k]));
assert.equal(json[k].length, 36);
});
});
});
});
});