Merge pull request #12586 from BerkeleyTrue/feat/real-time-user-count

feat(api): add current active users api
pull/12909/head
Quincy Larson 2017-01-26 22:02:02 -06:00 committed by GitHub
commit 1b2b54e2b0
9 changed files with 174 additions and 20 deletions

1
.gitignore vendored
View File

@ -53,3 +53,4 @@ public/js/frame-runner*
public/css/main*
server/rev-manifest.json
google-credentials.json

View File

@ -108,22 +108,24 @@
"default": "{}"
},
"required": {
"type": [{
"type": {
"link": {
"type": "string",
"description": "Used for css files"
},
"src": {
"type": "string",
"description": "Used for script files"
},
"crossDomain": {
"type": "boolean",
"description": "Files coming from FreeCodeCamp must mark this true"
"type": [
{
"type": {
"link": {
"type": "string",
"description": "Used for css files"
},
"src": {
"type": "string",
"description": "Used for script files"
},
"crossDomain": {
"type": "boolean",
"description": "Files coming from FreeCodeCamp must mark this true"
}
}
}
}],
],
"default": []
}
},

View File

@ -13,9 +13,7 @@
}
},
"validations": [],
"relations": {
},
"relations": {},
"acls": [
{
"accessType": "*",
@ -30,5 +28,5 @@
"permission": "ALLOW"
}
],
"methods": []
"methods": {}
}

View File

@ -122,8 +122,8 @@
},
"currentChallengeId": {
"type": "string",
"default": "",
"description": "The challenge last visited by the user"
"description": "The challenge last visited by the user",
"default": ""
},
"currentChallenge": {
"type": {},

View File

@ -56,6 +56,7 @@
"fetchr": "~0.5.12",
"frameguard": "^3.0.0",
"gitter-sidecar": "^1.2.3",
"googleapis": "16.1.0",
"helmet": "^3.1.0",
"helmet-csp": "^2.1.0",
"history": "^3.2.1",

View File

@ -74,5 +74,9 @@
"block": {
"dataSource": "db",
"public": true
},
"about": {
"dataSource": "db",
"public": true
}
}

25
server/models/about.js Normal file
View File

@ -0,0 +1,25 @@
import { createActiveUsers } from '../utils/about.js';
module.exports = function(About) {
const activeUsers = createActiveUsers();
About.getActiveUsers = function getActiveUsers() {
// converting to promise automatically will subscribe to Observable
// initiating the sequence above
return activeUsers.toPromise();
};
About.remoteMethod(
'getActiveUsers',
{
http: {
path: '/get-active-users',
verb: 'get'
},
returns: {
type: 'number',
arg: 'activeUsers'
}
}
);
};

28
server/models/about.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "about",
"plural": "about",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW",
"property": "getActiveUsers"
}
],
"methods": {}
}

95
server/utils/about.js Normal file
View File

@ -0,0 +1,95 @@
import _ from 'lodash';
import debug from 'debug';
import dedent from 'dedent';
import fs from 'fs';
import goog from 'googleapis';
import { Observable } from 'rx';
import { timeCache, observeMethod } from './rx';
// one million!
const upperBound = 1000 * 1000;
const scope = 'https://www.googleapis.com/auth/analytics.readonly';
const pathToCred = process.env.GOOGLE_APPLICATION_CREDENTIALS;
const log = debug('fcc:server:utils:about');
const analytics = goog.analytics('v3');
const makeRequest = observeMethod(analytics.data.realtime, 'get');
export const toBoundInt = _.flow(
// first convert string to integer
_.toInteger,
// then we bound the integer to prevent weird things like Infinity
// and negative numbers
// can't wait to the day we need to update this!
_.partialRight(_.clamp, 0, upperBound)
);
export function createActiveUsers() {
const zero = Observable.of(0);
let credentials;
if (!pathToCred) {
// if no path to credentials set to zero;
log(dedent`
no google applications credentials environmental variable found
'GOOGLE_APPLICATION_CREDENTIALS'
'activeUser' api will always return 0
this can safely be ignored during development
`);
return zero;
}
try {
credentials = require(fs.realpathSync(pathToCred));
} catch (err) {
log('google applications credentials file failed to require');
console.error(err);
// if we can't require credentials set to zero;
return zero;
}
if (
!credentials.private_key ||
!credentials.client_email ||
!credentials.viewId
) {
log(dedent`
google applications credentials json should have a
* private_key
* client_email
* viewId
but none were found
`);
return zero;
}
const client = new goog.auth.JWT(
credentials['client_email'],
null,
credentials['private_key'],
[scope],
);
const authorize = observeMethod(client, 'authorize');
const options = {
ids: `ga:${credentials.viewId}`,
auth: client,
metrics: 'rt:activeUsers'
};
return Observable.defer(
// we wait for authorize to complete before attempting to make request
// this ensures our token is initialized and valid
// we defer here to make sure the actual request is done per subscription
// instead of once at startup
() => authorize().flatMap(() => makeRequest(options))
)
// data: Array[body|Object, request: Request]
.map(data => data[0])
.map(
({ totalsForAllResults } = {}) => totalsForAllResults['rt:activeUsers']
)
.map(toBoundInt)
// print errors to error log for logging, duh
.do(null, err => console.error(err))
// always send a number down
.catch(() => Observable.of(0))
// cache for 2 seconds to prevent hitting our daily request limit
::timeCache(2, 'seconds');
}