Merge pull request #12586 from BerkeleyTrue/feat/real-time-user-count
feat(api): add current active users apipull/12909/head
commit
1b2b54e2b0
|
@ -53,3 +53,4 @@ public/js/frame-runner*
|
|||
public/css/main*
|
||||
|
||||
server/rev-manifest.json
|
||||
google-credentials.json
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
},
|
||||
|
|
|
@ -13,9 +13,7 @@
|
|||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
|
||||
},
|
||||
"relations": {},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
|
@ -30,5 +28,5 @@
|
|||
"permission": "ALLOW"
|
||||
}
|
||||
],
|
||||
"methods": []
|
||||
"methods": {}
|
||||
}
|
||||
|
|
|
@ -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": {},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -74,5 +74,9 @@
|
|||
"block": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"about": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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": {}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
|
Loading…
Reference in New Issue