Merge pull request #45 from lbryio/analytics-v2

Analytics v2
This commit is contained in:
Bill Bittner 2017-06-29 16:14:28 -07:00 committed by GitHub
commit 9a2fa9efe9
13 changed files with 91 additions and 78 deletions

View file

@ -2,11 +2,11 @@ const logger = require('winston');
const db = require('../models'); const db = require('../models');
module.exports = { module.exports = {
getAnalyticsSummary: () => { getStatsSummary: () => {
logger.debug('retrieving analytics'); logger.debug('retrieving site statistics');
const deferred = new Promise((resolve, reject) => { const deferred = new Promise((resolve, reject) => {
// get the raw analytics data // get the raw statistics data
db.Analytics db.Stats
.findAll() .findAll()
.then(data => { .then(data => {
const resultHashTable = {}; const resultHashTable = {};

View file

@ -1,20 +1,20 @@
const logger = require('winston'); const logger = require('winston');
const { postToAnalytics } = require('./analytics'); const { postToStats } = require('./statsHelpers.js');
module.exports = { module.exports = {
handleRequestError (action, originalUrl, ip, error, res) { handleRequestError (action, originalUrl, ip, error, res) {
logger.error('Request Error >>', error); logger.error('Request Error >>', error);
if (error === 'NO_CLAIMS' || error === 'NO_FREE_PUBLIC_CLAIMS') { if (error === 'NO_CLAIMS' || error === 'NO_FREE_PUBLIC_CLAIMS') {
postToAnalytics(action, originalUrl, ip, 'success'); postToStats(action, originalUrl, ip, 'success');
res.status(307).render('noClaims'); res.status(307).render('noClaims');
} else if (error.response) { } else if (error.response) {
postToAnalytics(action, originalUrl, ip, error.response.data.error.messsage); postToStats(action, originalUrl, ip, error.response.data.error.messsage);
res.status(error.response.status).send(error.response.data.error.message); res.status(error.response.status).send(error.response.data.error.message);
} else if (error.code === 'ECONNREFUSED') { } else if (error.code === 'ECONNREFUSED') {
postToAnalytics(action, originalUrl, ip, 'Connection refused. The daemon may not be running.'); postToStats(action, originalUrl, ip, 'Connection refused. The daemon may not be running.');
res.status(503).send('Connection refused. The daemon may not be running.'); res.status(503).send('Connection refused. The daemon may not be running.');
} else { } else {
postToAnalytics(action, originalUrl, ip, error); postToStats(action, originalUrl, ip, error);
res.status(400).send(JSON.stringify(error)); res.status(400).send(JSON.stringify(error));
} }
}, },

View file

@ -2,8 +2,8 @@ const db = require('../../models');
const logger = require('winston'); const logger = require('winston');
module.exports = { module.exports = {
postToAnalytics: (action, url, ipAddress, result) => { postToStats: (action, url, ipAddress, result) => {
logger.silly('creating record for analytics'); logger.silly('creating record for statistics db');
// make sure the result is a string // make sure the result is a string
if (result && (typeof result !== 'string')) { if (result && (typeof result !== 'string')) {
result = result.toString(); result = result.toString();
@ -13,7 +13,7 @@ module.exports = {
ipAddress = ipAddress.toString(); ipAddress = ipAddress.toString();
} }
// create record in the db // create record in the db
db.Analytics.create({ db.Stats.create({
action, action,
url, url,
ipAddress, ipAddress,

View file

@ -1,6 +1,6 @@
module.exports = (sequelize, { STRING, TEXT }) => { module.exports = (sequelize, { STRING, TEXT }) => {
const Analytics = sequelize.define( const Stats = sequelize.define(
'Analytics', 'Stats',
{ {
action: { action: {
type : STRING, type : STRING,
@ -25,5 +25,5 @@ module.exports = (sequelize, { STRING, TEXT }) => {
freezeTableName: true, freezeTableName: true,
} }
); );
return Analytics; return Stats;
}; };

View file

@ -65,7 +65,7 @@ canvas {
float: left; float: left;
} }
/* analytics */ /* statistics */
.totals-row { .totals-row {
border-top: 1px solid grey; border-top: 1px solid grey;
border-bottom: 1px solid grey; border-bottom: 1px solid grey;

View file

@ -1,17 +0,0 @@
const errorHandlers = require('../helpers/libraries/errorHandlers.js');
const analyticsController = require('../controllers/analyticsController.js');
module.exports = (app) => {
// route to show analytics
app.get('/analytics', (req, res) => {
// get and serve content
analyticsController
.getAnalyticsSummary()
.then(result => {
res.status(200).render('analytics', result);
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
});
});
};

View file

@ -5,16 +5,16 @@ const publishController = require('../controllers/publishController.js');
const lbryApi = require('../helpers/libraries/lbryApi.js'); const lbryApi = require('../helpers/libraries/lbryApi.js');
const publishHelpers = require('../helpers/libraries/publishHelpers.js'); const publishHelpers = require('../helpers/libraries/publishHelpers.js');
const errorHandlers = require('../helpers/libraries/errorHandlers.js'); const errorHandlers = require('../helpers/libraries/errorHandlers.js');
const { postToAnalytics } = require('../helpers/libraries/analytics'); const { postToStats } = require('../helpers/libraries/statsHelpers.js');
module.exports = app => { module.exports = app => {
// route to run a claim_list request on the daemon // route to run a claim_list request on the daemon
app.get('/api/claim_list/:claim', ({ originalUrl, params, ip }, res) => { app.get('/api/claim_list/:claim', ({ originalUrl, params, ip }, res) => {
logger.debug(`GET request on ${originalUrl} from ${ip}`); logger.verbose(`GET request on ${originalUrl} from ${ip}`);
lbryApi lbryApi
.getClaimsList(params.claim) .getClaimsList(params.claim)
.then(claimsList => { .then(claimsList => {
postToAnalytics('publish', originalUrl, ip, 'success'); postToStats('publish', originalUrl, ip, 'success');
res.status(200).json(claimsList); res.status(200).json(claimsList);
}) })
.catch(error => { .catch(error => {
@ -23,11 +23,11 @@ module.exports = app => {
}); });
// route to run a resolve request on the daemon // route to run a resolve request on the daemon
app.get('/api/resolve/:uri', ({ originalUrl, params, ip }, res) => { app.get('/api/resolve/:uri', ({ originalUrl, params, ip }, res) => {
logger.debug(`GET request on ${originalUrl} from ${ip}`); logger.verbose(`GET request on ${originalUrl} from ${ip}`);
lbryApi lbryApi
.resolveUri(params.uri) .resolveUri(params.uri)
.then(resolvedUri => { .then(resolvedUri => {
postToAnalytics('publish', originalUrl, ip, 'success'); postToStats('publish', originalUrl, ip, 'success');
res.status(200).json(resolvedUri); res.status(200).json(resolvedUri);
}) })
.catch(error => { .catch(error => {
@ -36,11 +36,11 @@ module.exports = app => {
}); });
// route to run a publish request on the daemon // route to run a publish request on the daemon
app.post('/api/publish', multipartMiddleware, ({ originalUrl, body, files, ip }, res) => { app.post('/api/publish', multipartMiddleware, ({ originalUrl, body, files, ip }, res) => {
logger.debug(`POST request on ${originalUrl} from ${ip}`); logger.verbose(`POST request on ${originalUrl} from ${ip}`);
// validate that a file was provided // validate that a file was provided
const file = files.speech || files.null; const file = files.speech || files.null;
if (!file) { if (!file) {
postToAnalytics('publish', originalUrl, ip, 'Error: file'); postToStats('publish', originalUrl, ip, 'Error: file');
res.status(400).send('Error: No file was submitted or the key used was incorrect. Files posted through this route must use a key of "speech" or null'); res.status(400).send('Error: No file was submitted or the key used was incorrect. Files posted through this route must use a key of "speech" or null');
return; return;
} }
@ -48,14 +48,14 @@ module.exports = app => {
const name = body.name || file.name.substring(0, file.name.indexOf('.')); const name = body.name || file.name.substring(0, file.name.indexOf('.'));
const invalidCharacters = /[^\w,-]/.exec(name); const invalidCharacters = /[^\w,-]/.exec(name);
if (invalidCharacters) { if (invalidCharacters) {
postToAnalytics('publish', originalUrl, ip, 'Error: name'); postToStats('publish', originalUrl, ip, 'Error: name');
res.status(400).send('Error: The name you provided is not allowed. Please use A-Z, a-z, 0-9, "_" and "-" only.'); res.status(400).send('Error: The name you provided is not allowed. Please use A-Z, a-z, 0-9, "_" and "-" only.');
return; return;
} }
// validate license // validate license
const license = body.license || 'No License Provided'; const license = body.license || 'No License Provided';
if ((license.indexOf('Public Domain') === -1) && (license.indexOf('Creative Commons') === -1)) { if ((license.indexOf('Public Domain') === -1) && (license.indexOf('Creative Commons') === -1)) {
postToAnalytics('puplish', originalUrl, ip, 'Error: license'); postToStats('puplish', originalUrl, ip, 'Error: license');
res.status(400).send('Error: Only posts with a license of "Public Domain" or "Creative Commons" are eligible for publishing through spee.ch'); res.status(400).send('Error: Only posts with a license of "Public Domain" or "Creative Commons" are eligible for publishing through spee.ch');
return; return;
} }
@ -73,7 +73,7 @@ module.exports = app => {
case '1': case '1':
break; break;
default: default:
postToAnalytics('publish', originalUrl, ip, 'Error: nsfw'); postToStats('publish', originalUrl, ip, 'Error: nsfw');
res.status(400).send('Error: NSFW value was not accepted. NSFW must be set to either true, false, "on", or "off"'); res.status(400).send('Error: NSFW value was not accepted. NSFW must be set to either true, false, "on", or "off"');
return; return;
} }
@ -89,7 +89,7 @@ module.exports = app => {
publishController publishController
.publish(publishParams, fileName, fileType) .publish(publishParams, fileName, fileType)
.then(result => { .then(result => {
postToAnalytics('publish', originalUrl, ip, 'success'); postToStats('publish', originalUrl, ip, 'success');
res.status(200).json(result); res.status(200).json(result);
}) })
.catch(error => { .catch(error => {

View file

@ -1,17 +1,16 @@
const logger = require('winston'); const logger = require('winston');
const { postToAnalytics } = require('../helpers/libraries/analytics'); const { postToStats } = require('../helpers/libraries/statsHelpers.js');
module.exports = app => { module.exports = app => {
// route for the home page // route for the home page
app.get('/', ({ originalUrl, ip, headers }, res) => { app.get('/', ({ originalUrl, ip, headers }, res) => {
logger.verbose(`GET request on ${originalUrl} from ${ip}`); logger.verbose(`GET request on ${originalUrl} from ${ip}`);
logger.debug(`headers ${JSON.stringify(headers)}`);
res.status(200).render('index'); res.status(200).render('index');
}); });
// a catch-all route if someone visits a page that does not exist // a catch-all route if someone visits a page that does not exist
app.use('*', ({ originalUrl, ip }, res) => { app.use('*', ({ originalUrl, ip }, res) => {
logger.error(`Get request on ${originalUrl} from ${ip} which was a 404`); logger.error(`Get request on ${originalUrl} from ${ip} which was a 404`);
postToAnalytics('post', originalUrl, ip, 'Error: 404'); postToStats('post', originalUrl, ip, 'Error: 404');
res.status(404).render('fourOhFour'); res.status(404).render('fourOhFour');
}); });
}; };

View file

@ -1,7 +1,21 @@
const errorHandlers = require('../helpers/libraries/errorHandlers.js'); const errorHandlers = require('../helpers/libraries/errorHandlers.js');
const serveController = require('../controllers/serveController.js'); const serveController = require('../controllers/serveController.js');
const logger = require('winston'); const logger = require('winston');
const { postToAnalytics } = require('../helpers/libraries/analytics'); const { postToStats } = require('../helpers/libraries/statsHelpers.js');
function sendGoogleAnalytics (ua, googleApiKey, ip, originalUrl) {
const visitorId = ip.replace(/\./g, '-');
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
visitor.pageview(originalUrl, 'https://spee.ch', 'Serve Route', (err) => {
if (err) {
logger.error('Google Analytics Pageview Error >>', err);
}
}).event('Serve', originalUrl, (err) => {
if (err) {
logger.error('Google Analytics Event Error >>', err);
}
});
}
function serveFile ({ fileName, fileType, filePath }, res) { function serveFile ({ fileName, fileType, filePath }, res) {
logger.info(`serving file ${fileName}`); logger.info(`serving file ${fileName}`);
@ -32,37 +46,39 @@ function serveFile ({ fileName, fileType, filePath }, res) {
res.status(200).sendFile(filePath, options); res.status(200).sendFile(filePath, options);
} }
module.exports = (app) => { module.exports = (app, ua, googleApiKey) => {
// route to fetch one free public claim // route to fetch one free public claim
app.get('/:name/:claim_id', ({ originalUrl, params, ip, ips, headers }, res) => { app.get('/:name/:claim_id', ({ originalUrl, params, ip }, res) => {
// google analytics
sendGoogleAnalytics(ua, googleApiKey, ip, originalUrl);
// logging
logger.verbose(`GET request on ${originalUrl} from ${ip}`); logger.verbose(`GET request on ${originalUrl} from ${ip}`);
logger.debug(`ips >> ${JSON.stringify(ips)}`);
logger.debug(`headers >> ${JSON.stringify(headers)}`);
// begin image-serve processes // begin image-serve processes
serveController serveController
.getClaimByClaimId(params.name, params.claim_id) .getClaimByClaimId(params.name, params.claim_id)
.then(fileInfo => { .then(fileInfo => {
postToAnalytics('serve', originalUrl, ips, 'success'); postToStats('serve', originalUrl, ip, 'success');
serveFile(fileInfo, res); serveFile(fileInfo, res);
}) })
.catch(error => { .catch(error => {
errorHandlers.handleRequestError('serve', originalUrl, ips, error, res); errorHandlers.handleRequestError('serve', originalUrl, ip, error, res);
}); });
}); });
// route to fetch one free public claim // route to fetch one free public claim
app.get('/:name', ({ originalUrl, params, ip, ips, headers }, res) => { app.get('/:name', ({ originalUrl, params, ip }, res) => {
// google analytics
sendGoogleAnalytics(ua, googleApiKey, ip, originalUrl);
// logging
logger.verbose(`GET request on ${originalUrl} from ${ip}`); logger.verbose(`GET request on ${originalUrl} from ${ip}`);
logger.debug(`ips >> ${JSON.stringify(ips)}`);
logger.debug(`headers >> ${JSON.stringify(headers)}`);
// begin image-serve processes // begin image-serve processes
serveController serveController
.getClaimByName(params.name) .getClaimByName(params.name)
.then(fileInfo => { .then(fileInfo => {
postToAnalytics('serve', originalUrl, ips, 'success'); postToStats('serve', originalUrl, ip, 'success');
serveFile(fileInfo, res); serveFile(fileInfo, res);
}) })
.catch(error => { .catch(error => {
errorHandlers.handleRequestError('serve', originalUrl, ips, error, res); errorHandlers.handleRequestError('serve', originalUrl, ip, error, res);
}); });
}); });
}; };

View file

@ -1,31 +1,46 @@
const logger = require('winston');
const errorHandlers = require('../helpers/libraries/errorHandlers.js'); const errorHandlers = require('../helpers/libraries/errorHandlers.js');
const showController = require('../controllers/showController.js'); const showController = require('../controllers/showController.js');
const logger = require('winston'); const { postToStats } = require('../helpers/libraries/statsHelpers.js');
const { postToAnalytics } = require('../helpers/libraries/analytics'); const statsController = require('../controllers/statsController.js');
module.exports = (app) => { module.exports = (app) => {
// route to fetch all free public claims // route to show the meme-fodder meme maker
app.get('/meme-fodder/play', ({ originalUrl, ip }, res) => { app.get('/meme-fodder/play', ({ originalUrl, ip }, res) => {
logger.debug(`GET request on ${originalUrl} from ${ip}`); logger.verbose(`POST request on ${originalUrl} from ${ip}`);
// get and serve content // get and serve the content
showController showController
.getAllClaims('meme-fodder') .getAllClaims('meme-fodder')
.then(orderedFreePublicClaims => { .then(orderedFreePublicClaims => {
postToAnalytics('show', originalUrl, ip, 'success'); postToStats('show', originalUrl, ip, 'success');
res.status(200).render('memeFodder', { claims: orderedFreePublicClaims }); res.status(200).render('memeFodder', { claims: orderedFreePublicClaims });
}) })
.catch(error => { .catch(error => {
errorHandlers.handleRequestError('show', originalUrl, ip, error, res); errorHandlers.handleRequestError('show', originalUrl, ip, error, res);
}); });
}); });
// route to fetch all free public claims // route to show statistics for spee.ch
app.get('/stats', ({ originalUrl, ip }, res) => {
logger.verbose(`POST request on ${originalUrl} from ${ip}`);
// get and serve the content
statsController
.getStatsSummary()
.then(result => {
postToStats('show', originalUrl, ip, 'success');
res.status(200).render('statistics', result);
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
});
});
// route to display all free public claims at a given name
app.get('/:name/all', ({ originalUrl, params, ip }, res) => { app.get('/:name/all', ({ originalUrl, params, ip }, res) => {
logger.debug(`GET request on ${originalUrl} from ${ip}`); logger.verbose(`POST request on ${originalUrl} from ${ip}`);
// get and serve content // get and serve the content
showController showController
.getAllClaims(params.name) .getAllClaims(params.name)
.then(orderedFreePublicClaims => { .then(orderedFreePublicClaims => {
postToAnalytics('show', originalUrl, ip, 'success'); postToStats('show', originalUrl, ip, 'success');
res.status(200).render('allClaims', { claims: orderedFreePublicClaims }); res.status(200).render('allClaims', { claims: orderedFreePublicClaims });
}) })
.catch(error => { .catch(error => {

View file

@ -2,7 +2,7 @@ const logger = require('winston');
const publishController = require('../controllers/publishController.js'); const publishController = require('../controllers/publishController.js');
const publishHelpers = require('../helpers/libraries/publishHelpers.js'); const publishHelpers = require('../helpers/libraries/publishHelpers.js');
const errorHandlers = require('../helpers/libraries/errorHandlers.js'); const errorHandlers = require('../helpers/libraries/errorHandlers.js');
const { postToAnalytics } = require('../helpers/libraries/analytics'); const { postToStats } = require('../helpers/libraries/statsHelpers.js');
module.exports = (app, siofu, hostedContentPath) => { module.exports = (app, siofu, hostedContentPath) => {
const http = require('http').Server(app); const http = require('http').Server(app);
@ -25,7 +25,7 @@ module.exports = (app, siofu, hostedContentPath) => {
// listener for when file upload encounters an error // listener for when file upload encounters an error
uploader.on('error', ({ error }) => { uploader.on('error', ({ error }) => {
logger.error('an error occured while uploading', error); logger.error('an error occured while uploading', error);
postToAnalytics('publish', '/', null, error); postToStats('publish', '/', null, error);
socket.emit('publish-status', error); socket.emit('publish-status', error);
}); });
// listener for when file has been uploaded // listener for when file has been uploaded
@ -39,18 +39,18 @@ module.exports = (app, siofu, hostedContentPath) => {
publishController publishController
.publish(publishParams, file.name, file.meta.type) .publish(publishParams, file.name, file.meta.type)
.then(result => { .then(result => {
postToAnalytics('publish', '/', null, 'success'); postToStats('publish', '/', null, 'success');
socket.emit('publish-complete', { name: publishParams.name, result }); socket.emit('publish-complete', { name: publishParams.name, result });
}) })
.catch(error => { .catch(error => {
error = errorHandlers.handlePublishError(error); error = errorHandlers.handlePublishError(error);
postToAnalytics('publish', '/', null, error); postToStats('publish', '/', null, error);
socket.emit('publish-failure', error); socket.emit('publish-failure', error);
}); });
} else { } else {
logger.error(`An error occurred in uploading the client's file`); logger.error(`An error occurred in uploading the client's file`);
socket.emit('publish-failure', 'file uploaded, but with errors'); socket.emit('publish-failure', 'file uploaded, but with errors');
postToAnalytics('publish', '/', null, 'file uploaded, but with errors'); postToStats('publish', '/', null, 'file uploaded, but with errors');
// to-do: remove the file if not done automatically // to-do: remove the file if not done automatically
} }
}); });

View file

@ -6,7 +6,8 @@ const expressHandlebars = require('express-handlebars');
const Handlebars = require('handlebars'); const Handlebars = require('handlebars');
const config = require('config'); const config = require('config');
const winston = require('winston'); const winston = require('winston');
const ua = require('universal-analytics');
const googleApiKey = config.get('AnalyticsConfig.GoogleId');
const hostedContentPath = config.get('Database.PublishUploadPath'); const hostedContentPath = config.get('Database.PublishUploadPath');
// configure logging // configure logging
@ -56,9 +57,8 @@ app.set('view engine', 'handlebars');
// require express routes // require express routes
require('./routes/api-routes.js')(app); require('./routes/api-routes.js')(app);
require('./routes/analytics-routes.js')(app);
require('./routes/show-routes.js')(app); require('./routes/show-routes.js')(app);
require('./routes/serve-routes.js')(app); require('./routes/serve-routes.js')(app, ua, googleApiKey);
require('./routes/home-routes.js')(app); require('./routes/home-routes.js')(app);
// require socket.io routes // require socket.io routes

View file

@ -3,7 +3,7 @@
{{> topBar}} {{> topBar}}
</div> </div>
<div> <div>
<h3>Analytics</h3> <h3>Site Statistics</h3>
<table> <table>
<tr> <tr>
<th>action</th> <th>action</th>