Merge pull request #12586 from BerkeleyTrue/feat/real-time-user-count
feat(api): add current active users api
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -53,3 +53,4 @@ public/js/frame-runner*
|
|||||||
public/css/main*
|
public/css/main*
|
||||||
|
|
||||||
server/rev-manifest.json
|
server/rev-manifest.json
|
||||||
|
google-credentials.json
|
||||||
|
@ -108,22 +108,24 @@
|
|||||||
"default": "{}"
|
"default": "{}"
|
||||||
},
|
},
|
||||||
"required": {
|
"required": {
|
||||||
"type": [{
|
"type": [
|
||||||
"type": {
|
{
|
||||||
"link": {
|
"type": {
|
||||||
"type": "string",
|
"link": {
|
||||||
"description": "Used for css files"
|
"type": "string",
|
||||||
},
|
"description": "Used for css files"
|
||||||
"src": {
|
},
|
||||||
"type": "string",
|
"src": {
|
||||||
"description": "Used for script files"
|
"type": "string",
|
||||||
},
|
"description": "Used for script files"
|
||||||
"crossDomain": {
|
},
|
||||||
"type": "boolean",
|
"crossDomain": {
|
||||||
"description": "Files coming from FreeCodeCamp must mark this true"
|
"type": "boolean",
|
||||||
|
"description": "Files coming from FreeCodeCamp must mark this true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}],
|
],
|
||||||
"default": []
|
"default": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -13,9 +13,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validations": [],
|
"validations": [],
|
||||||
"relations": {
|
"relations": {},
|
||||||
|
|
||||||
},
|
|
||||||
"acls": [
|
"acls": [
|
||||||
{
|
{
|
||||||
"accessType": "*",
|
"accessType": "*",
|
||||||
@ -30,5 +28,5 @@
|
|||||||
"permission": "ALLOW"
|
"permission": "ALLOW"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"methods": []
|
"methods": {}
|
||||||
}
|
}
|
||||||
|
@ -122,8 +122,8 @@
|
|||||||
},
|
},
|
||||||
"currentChallengeId": {
|
"currentChallengeId": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "",
|
"description": "The challenge last visited by the user",
|
||||||
"description": "The challenge last visited by the user"
|
"default": ""
|
||||||
},
|
},
|
||||||
"currentChallenge": {
|
"currentChallenge": {
|
||||||
"type": {},
|
"type": {},
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
"fetchr": "~0.5.12",
|
"fetchr": "~0.5.12",
|
||||||
"frameguard": "^3.0.0",
|
"frameguard": "^3.0.0",
|
||||||
"gitter-sidecar": "^1.2.3",
|
"gitter-sidecar": "^1.2.3",
|
||||||
|
"googleapis": "16.1.0",
|
||||||
"helmet": "^3.1.0",
|
"helmet": "^3.1.0",
|
||||||
"helmet-csp": "^2.1.0",
|
"helmet-csp": "^2.1.0",
|
||||||
"history": "^3.2.1",
|
"history": "^3.2.1",
|
||||||
|
@ -74,5 +74,9 @@
|
|||||||
"block": {
|
"block": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"dataSource": "db",
|
||||||
|
"public": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
server/models/about.js
Normal file
25
server/models/about.js
Normal 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
28
server/models/about.json
Normal 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
95
server/utils/about.js
Normal 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');
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user