Skip to content

Commit

Permalink
Merge pull request mozilla#44 from mozilla/nimbledroid_data_fetching
Browse files Browse the repository at this point in the history
Fetch Nimbledroid data outside of endpoint
  • Loading branch information
armenzg committed Jul 23, 2018
2 parents 205bdb0 + 101b452 commit 980f571
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .neutrinorc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
module.exports = {
options: {
mains: {
index: 'index',
nimbledroid: 'scripts/fetchNimbledroidData.js',
}
},
use: [
[
'@neutrinojs/airbnb-base',
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ yarn // To get the dependencies installed
yarn start // To start the server
```

### Providing a Google API key
### Enable access to the Google status spreadsheet

The [notes](http://localhost:3000/api/perf/notes) API requires a `GOOGLE_API_KEY`
in order to access a Google Spreadsheet. In order for this API to work locally
Expand All @@ -39,7 +39,7 @@ GOOGLE_API_KEY=<created API key> yarn start
```
* Visit http://localhost:3000/api/perf/notes to verify it works

### Providing a Nimbledroid API key
### Enable access to Nimbledroid's data
Nimbledroid provides us with performance data for various sites on Android.
If you want to make changes to the Nimbledroid APIs on the backend you will need
to have access to our corporate Nimbledroid account.
Expand All @@ -52,10 +52,15 @@ Once you have it you can start the backend like this:
```
export NIMBLEDROID_API_KEY=<API key>
export NIMBLEDROID_EMAIL=<your email address>
yarn fetchNimbledroidData
yarn start
```

Load http://localhost:3000/api/nimbledroid to verify it works.
Load [this page](http://localhost:3000/api/nimbledroid?product=focus) to verify it works.

### Redis

If you want to test caching with Redis (there's caching with JS as a fallback) make sure to install Redis and set the REDIS_URL env to `redis://localhost:6379` before starting the server.

## Attributions

Expand Down
1 change: 1 addition & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "firefox-health-backend",
"scripts": {
"postdeploy": "yarn fetchNimbledroidData"
},
"env": {
"GOOGLE_API_KEY": {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"start:prod": "neutrino build && node .",
"start:debugger": "neutrino build && node --inspect .",
"precommit": "lint-staged",
"heroku-postbuild": "npm run build"
"heroku-postbuild": "npm run build",
"fetchNimbledroidData": "node build/nimbledroid.js"
},
"lint-staged": {
"*.js": [
Expand Down
25 changes: 15 additions & 10 deletions src/android/routes.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import Router from 'koa-router';
import NimbledroidClient from '../utils/NimbledroidClient';
import queryNimbledroidData from '../utils/apis/queryNimbledroidData';
import { getSpreadsheetValues } from '../utils/google';
import config from '../configuration';

const README_URL = `${config.url}/blob/master/README.md`;

export const router = new Router();

router
.get('/klar', async (ctx) => {
if (!process.env.GOOGLE_API_KEY) {
ctx.throw(
500,
'You need to set the GOOGLE_API_KEY for this endpoint to work. More info in ' +
'https://github.com/mozilla/firefox-health-backend/blob/master/README.md',
'You need to set the GOOGLE_API_KEY for this endpoint to work. ' +
`More info in ${README_URL}`,
);
}
const { site } = ctx.request.query;
Expand All @@ -32,8 +34,15 @@ router
if (!process.env.NIMBLEDROID_API_KEY || !process.env.NIMBLEDROID_EMAIL) {
ctx.throw(
400,
'You need to set Nimbledroid authentication for this endpoint to work. More info in ' +
'https://github.com/mozilla/firefox-health-backend/blob/master/README.md',
'You need to set Nimbledroid authentication for this endpoint to work. ' +
`More info in ${README_URL}`,
);
}
if (!process.env.REDIS_URL) {
ctx.throw(
400,
'You need to run Redis for this endpoint to work. ' +
`More info in ${README_URL}`,
);
}
const { product } = ctx.request.query;
Expand All @@ -43,9 +52,5 @@ router
'You need to call this endpoint with ?product=<klar|focus>.',
);
}
const handler = new NimbledroidClient(
process.env.NIMBLEDROID_EMAIL,
process.env.NIMBLEDROID_API_KEY,
);
ctx.body = await handler.getNimbledroidData(product);
ctx.body = await queryNimbledroidData(product);
});
3 changes: 1 addition & 2 deletions src/fetch/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ const redisFetch = async (url, options) => {
if (text) {
await storeInRedis(key, text, options);
}
console.log(text);
return text;
};

Expand All @@ -58,7 +57,7 @@ const inMemoryFetch = (url, options) => {

const fetchText = async (url, options = {}) => {
let text;
if (process.env.REDIS_URL) {
if (process.env.REDIS_URL && db) {
try {
text = redisFetch(url, options);
} catch (e) {
Expand Down
65 changes: 65 additions & 0 deletions src/scripts/fetchNimbledroidData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import asyncRedis from 'async-redis';
import NimbledroidClient from '../utils/NimbledroidClient';

if (
!process.env.REDIS_URL ||
!process.env.NIMBLEDROID_API_KEY ||
!process.env.NIMBLEDROID_EMAIL
) {
throw Error('You need to set NIMBLEDROID_EMAIL, NIMBLEDROID_API_KEY and REDIS_URL');
}

const nimbledroidClient = new NimbledroidClient(
process.env.NIMBLEDROID_EMAIL,
process.env.NIMBLEDROID_API_KEY,
);
const redisClient = asyncRedis.createClient(process.env.REDIS_URL);

redisClient.on('error', (err) => {
console.error(err);
});

// eslint-disable-next-line consistent-return
const storeProfilingRunIfMissing = async (profilingRunData) => {
const KNOWN_STATUS = ['Crawling', 'Failed', 'Profiling', 'Profiled'];
const { status, url } = profilingRunData;
if (!KNOWN_STATUS.includes(status)) {
throw Error(`Status: ${status} is new to us; Handle it in the code.`);
}

// e.g. cache:https://nimbledroid.com/api/v2/users/npark@mozilla.com/apps/org.mozilla.klar/apks/103
const key = `cache:${url}`;
// The status 'Failed' means 'completed' in the Nimbledroid API
if (status === 'Failed') {
const cached = await redisClient.get(key);
if (!cached) {
console.log(`Storing ${key}`);
await redisClient.set(key, JSON.stringify(profilingRunData));
}
}
};

const storeDataInRedis = async (data) => {
await Promise.all(Object.keys(data).map(index =>
storeProfilingRunIfMissing(data[index])));
};

const fetchData = async productName =>
nimbledroidClient.getNimbledroidData(productName);

const main = async () => {
console.log('Fetching each product can take between 20-40 seconds.');
try {
await Promise.all(['klar', 'focus'].map(async (productName) => {
console.log(`Fetching ${productName}`);
const productData = await fetchData(productName);
await storeDataInRedis(productData);
}));
} catch (e) {
console.error(e);
} finally {
process.exit();
}
};

main();
6 changes: 5 additions & 1 deletion src/utils/NimbledroidClient/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ class NimbledroidHandler {
async fetchData(product) {
return fetchJson(
apiUrl(product),
{ method: 'GET', headers: this.generateAuthHeaders() },
{
method: 'GET',
headers: this.generateAuthHeaders(),
ttl: 30 * 60, // 30 minutes
},
);
}

Expand Down
18 changes: 18 additions & 0 deletions src/utils/apis/queryNimbledroidData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* This module produces the data needed for the api/android/nimbledroid endpoint
* The data can either be in Redis or in-memory
*/
import asyncRedis from 'async-redis';

const client = asyncRedis.createClient(process.env.REDIS_URL);

const queryNimbledroidData = async (product) => {
const cachedKeys = await client.keys(`cache:*nimble*${product}/apks/*`);
const data = await Promise.all(cachedKeys.map(async key =>
JSON.parse(await client.get(key))));
if (data.length === 0) {
throw Error('The script that fetches data should have run before hitting the API.');
}
return data;
};

export default queryNimbledroidData;
21 changes: 16 additions & 5 deletions test/android/nimbledroid.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* global afterEach describe, it */
/* global beforeEach describe, it */
import fetchMock from 'fetch-mock';
import superagent from 'supertest';
import app from '../../src/app';
Expand All @@ -13,28 +13,39 @@ describe('/android', () => {
fetchMock.get(`${config.nimbledroidApiUrl}.${product}/apks`, KLAR_DATA);

describe('GET /api/android/nimbledroid/', () => {
it('should return 400', (done) => {
it('No NIMBLEDROID_EMAIL should return 400', (done) => {
delete process.env.NIMBLEDROID_EMAIL;
request()
.get('/api/android/nimbledroid/')
.expect(400, done);
});

it('should return 400', (done) => {
it('No REDIS_URL should return 400', (done) => {
delete process.env.REDIS_URL;
request()
.get('/api/android/nimbledroid/')
.expect(400, done);
});

it('should return 200', (done) => {
it('No ?product=<foo> should return 400', (done) => {
request()
.get('/api/android/nimbledroid/')
.expect(400, done);
});

it.skip('should return 200', (done) => {
// XXX: If the data is now exclusively being retrieved via
// Redis then this backend is going to return an empty structure
// We should improve this test to actually be meaningful
request()
.get(`/api/android/nimbledroid/?product=${product}`)
.expect(200, done);
});

afterEach(() => {
beforeEach(() => {
process.env.NIMBLEDROID_EMAIL = 'nobody@moz.com';
process.env.NIMBLEDROID_API_KEY = 'foo_bar';
process.env.REDIS_URL = 'redis://localhost:fooPort';
});
});
});

0 comments on commit 980f571

Please sign in to comment.