Prepare to modify / refactor analytics (#970)
- Move analytics into their own file - Add test of analytics endpoint
This commit is contained in:
115
lib/analytics.js
Normal file
115
lib/analytics.js
Normal 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
127
server.js
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user